摘要Spring MVC 几乎每年都会被问烂但真正能把从 DispatchServlet 入口到视图渲染、异常处理、参数绑定、返回值解析这条链路讲清楚的人并不多。本文以一次 HTTP 请求为主线结合 Spring Framework 源码逐层拆解 DispatcherServlet、HandlerMapping、HandlerAdapter、HandlerMethodArgumentResolver、HttpMessageConverter、ViewResolver、HandlerExceptionResolver 等核心组件并附上高频面试题与完整时序图帮你把 Spring MVC 的执行过程彻底吃透。一、从一个最简单的 Spring MVC 项目说起很多同学背 Spring MVC 流程时张嘴就是“DispatcherServlet 收到请求然后交给 HandlerMapping 找到处理器再由 HandlerAdapter 调用最后返回 ModelAndView 交给视图解析器渲染”。这段话本身没错但一旦面试官追问“HandlerMapping 内部怎么找到的”“参数是怎么从 HttpServletRequest 绑定到 JavaBean 上的”“ResponseBody 为什么不走 ViewResolver”很多人就答不上来了。我们先搭一个最小可运行的例子后续所有源码分析都围绕它展开。一个标准的 Spring Boot Web 项目和传统 Spring MVC 项目底层执行流程完全一致只是配置方式不同。这里给出一个控制器javaRestController RequestMapping(/order) public class OrderController { GetMapping(/{id}) public ResultOrder getOrder(PathVariable(id) Long id, RequestParam(value source, required false) String source, RequestBody QueryCondition condition) { Order order orderService.findById(id); ResultOrder result Result.ok(order); result.setSource(source); return result; } PostMapping(/create) public String create(Validated RequestBody CreateOrderRequest request) { orderService.create(request); return redirect:/order/ request.getId(); } }上面的控制器几乎涵盖了 Spring MVC 的全部高频考点路径变量、请求参数、请求体 JSON、参数校验、返回 JSON、重定向。理解这一个类如何被调度就等于理解了 Spring MVC 九成以上的核心机制。下面我们从整体架构开始。二、Spring MVC 整体架构与核心组件Spring MVC 是围绕前端控制器模式Front Controller Pattern设计的。所有请求都先进入一个统一的 DispatcherServlet由它来完成请求分发、处理器定位、调用与响应输出。这种设计的好处是横切逻辑如权限校验、字符编码、异常处理、日志记录可以集中处理业务处理器只关心自己的逻辑。2.1 九大核心组件Spring MVC 框架把请求处理拆解到了多个可插拔的组件上每一个组件都是一个独立接口开发者都可以自定义替换。这九大组件在 DispatcherServlet 初始化时被加载具体名称如下组件接口默认实现职责HandlerMappingRequestMappingHandlerMapping根据请求找到处理器 Handler 及拦截器链HandlerAdapterRequestMappingHandlerAdapter调用处理器完成参数解析与返回值处理HandlerExceptionResolverExceptionHandlerExceptionResolver把处理过程中的异常解析为 ModelAndView 或响应数据ViewResolverInternalResourceViewResolver把逻辑视图名解析为真正的 View 对象LocaleResolverAcceptHeaderLocaleResolver解析国际化区域信息ThemeResolverFixedThemeResolver解析主题用于皮肤切换MultipartResolverStandardServletMultipartResolver处理文件上传请求FlashMapManagerSessionFlashMapManager管理重定向传参RequestToViewNameTranslatorDefaultRequestToViewNameTranslator在没有显式设置视图名时根据请求路径推导视图名其中与请求主链路最相关的五个是 HandlerMapping、HandlerAdapter、HandlerExceptionResolver、ViewResolver 和 MultipartResolver。面试中问得最集中的也是这五个尤其是前四个。2.2 DispatcherServlet 的继承体系DispatcherServlet 本质是一个 Servlet它的继承结构是DispatcherServlet 继承 FrameworkServletFrameworkServlet 继承 HttpServletBeanHttpServletBean 继承 HttpServlet。这个继承链的设计非常有意思每一层都解决一个问题javapublic abstract class HttpServletBean extends HttpServlet { // 负责把 ServletConfig 中的 init-param 注入到 Bean 属性 } public abstract class FrameworkServlet extends HttpServletBean { // 负责初始化 WebApplicationContext 容器 } public class DispatcherServlet extends FrameworkServlet { // 负责完成请求分发 }HttpServletBean 在 init 阶段读取 web.xml 或 ServletRegistration 中的 init-param通过反射把它们设置到 DispatcherServlet 的对应属性上。FrameworkServlet 则负责创建和刷新 Web 层的 Spring 容器即 WebApplicationContext。DispatcherServlet 在这一基础上初始化九大组件并在 service 阶段执行业务分发。之所以分这么多层是为了让继承体系职责单一如果你只是想扩展容器初始化逻辑可以继承 FrameworkServlet如果你想完全自定义一个特殊的分发器可以从 FrameworkServlet 派生而不必重复容器初始化代码。三、请求处理主流程一次请求的完整生命周期在深入源码之前先建立全貌认知。一次请求进入 Spring MVC 后大致经历下面这些步骤。这张图请一定要在心里过十遍因为面试时的“说说 Spring MVC 流程”本质就是在描述这个过程图中的核心方法就是 DispatcherServlet 的 doDispatch 方法。我把它的骨架代码提取出来去掉不关键的细节保留主干javaprotected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest request; HandlerExecutionChain mappedHandler null; boolean multipartRequestParsed false; try { ModelAndView mv null; Exception dispatchException null; // 1. 检查是否是文件上传请求如果是则解析 Multipart processedRequest checkMultipart(request); multipartRequestParsed (processedRequest ! request); // 2. 根据请求找到 Handler返回处理器执行链 mappedHandler getHandler(processedRequest); if (mappedHandler null) { noHandlerFound(processedRequest, response); return; } // 3. 根据 Handler 找到可以处理它的 HandlerAdapter HandlerAdapter ha getHandlerAdapter(mappedHandler.getHandler()); // 4. 执行拦截器的前置方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 5. 由适配器真正调用处理器 mv ha.handle(processedRequest, response, mappedHandler.getHandler()); // 6. 执行拦截器的后置方法 mappedHandler.applyPostHandle(processedRequest, response, mv); // 7. 处理分发结果渲染视图或处理异常 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { dispatchException ex; } finally { // 8. 无论如何执行拦截器的完成回调 if (mappedHandler ! null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } // 清理请求中的属性避免内存泄漏 if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } }虽然只是骨架代码但已经能清楚看到八大步骤。后面的章节我会逐个展开这些步骤背后的细节。特别要记住processDispatchResult 内部既负责正常结果的视图渲染也负责异常解析这就是为什么 Controller 里抛出的异常最终能被 ControllerAdvice 接住。四、HandlerMapping处理器映射的查找机制HandlerMapping 的职责只有一句话根据 HttpServletRequest 找到一个处理器执行链 HandlerExecutionChain。执行链里包含两个东西目标处理器 handler以及需要作用在这个请求上的拦截器列表 interceptors。4.1 HandlerMapping 接口定义javapublic interface HandlerMapping { HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; }接口极其简单只有一个 getHandler 方法。Spring 会持有多个 HandlerMapping 组成的列表在 DispatcherServlet.getHandler 中按顺序遍历。注意这里“按顺序”很关键多个映射器的优先级由 order 属性决定值越小越先匹配。默认的映射器列表大概是这样javaprotected ListHandlerMapping getDefaultStrategies(ApplicationContext context, ClassHandlerMapping strategyInterface) { return List.of( new RequestMappingHandlerMapping(), new BeanNameUrlHandlerMapping(), new RouterFunctionMapping(), new SimpleUrlHandlerMapping() ); }最常用的就是 RequestMappingHandlerMapping它基于 RequestMapping 注解完成映射。此外还有 BeanNameUrlHandlerMapping它把容器中名字以斜杠开头的 Bean比如名为 /hello 的 Bean映射为 URLSimpleUrlHandlerMapping 则通过显式的 URL 到 Bean 的配置映射。面试中只要重点掌握 RequestMappingHandlerMapping 即可。4.2 RequestMappingHandlerMapping 如何收集映射RequestMappingHandlerMapping 在初始化时会扫描容器中所有 Bean找出其中标注了 Controller 或 RequestMapping 的类型然后遍历这些类的方法把每一个 GetMapping、PostMapping、RequestMapping 注解信息解析为一个个 RequestMappingInfo 对象最终形成一张映射表。核心结构是javaprivate final MappingRegistry mappingRegistry new MappingRegistry(); // MappingRegistry 内部的核心数据结构 class MappingRegistry { // URL pattern - RequestMappingInfo private final MapT, MappingRegistrationT registry new HashMap(); // 路由集合按匹配复杂度排序 private final MultiValueMapString, T pathLookup new LinkedMultiValueMap(); // RequestMappingInfo - HandlerMethod private final MapT, HandlerMethod mappingLookup new LinkedHashMap(); }每一个 URL 模式都会对应一个由 RequestMappingInfo 封装的匹配条件集合里面包含请求路径、请求方法、请求参数、请求头、Content-Type、Accept 等条件。只有当所有条件同时匹配才认为这个映射命中了当前请求。以一个常见的注解为例javaGetMapping(value /{id}, params sourceweb, headers X-API-Version2) public ResultOrder getOrder(PathVariable Long id) { ... }这段注解会被解析成一个 RequestMappingInfo其中patternsCondition 为 /{id}methodsCondition 为 GETparamsCondition 要求请求参数 sourcewebheadersCondition 要求请求头 X-API-Version2。只有当请求四项都满足才会匹配到 getOrder 方法。4.3 路径匹配PathPattern 与 AntPathMatcherSpring 5.3 之前路径匹配默认使用 AntPathMatcher从 5.3 开始新的 PathPatternParser 成为默认。PathPattern 性能更好内存占用更低并且支持更严格的语义。AntPathMatcher 支持三种通配符? 匹配一个字符* 匹配路径中的一个片段** 匹配多个路径片段。PathPattern 的语法更严格它区分 {name} 路径变量、{*name} 捕获剩余路径、正则表达式等。面试中如果问到“Spring 路由是如何匹配的”可以回答先把 URL 解析为 PathPattern再对请求路径做 matchAndExtract提取路径变量供后续参数绑定使用。java// RequestMappingInfoHandlerMapping 中根据路径查找映射的简化逻辑 ListPathPattern matchingPatterns new ArrayList(); for (String pattern : getMappingPathPatterns(info)) { PathPattern pathPattern getPathPatternParser().parse(pattern); if (pathPattern.matches(lookupPath)) { matchingPatterns.add(pathPattern); } }当多个模式都能匹配同一个请求时Spring 会进行“最佳匹配排序”。排序规则大致是越具体的模式优先级越高。比如 /order/list 会优先于 /order/{id}/order/* 又优先于 /order/**。最终返回排序列表中的第一个映射。这个排序规则的解释在面试中也是个不错的加分项。五、HandlerExecutionChain 与拦截器注册找到 Handler 只是第一步getHandler 返回的是 HandlerExecutionChain它是 Handler 和拦截器链的组合。DispatcherServlet 在拿到执行链后会在合适的时机调用拦截器的三个回调方法 preHandle、postHandle、afterCompletion。5.1 HandlerExecutionChain 结构javapublic class HandlerExecutionChain { private final Object handler; private final ListHandlerInterceptor interceptorList new ArrayList(); private int interceptorIndex -1; boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { for (int i 0; i this.interceptorList.size(); i) { HandlerInterceptor interceptor this.interceptorList.get(i); if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex i; } return true; } }注意 applyPreHandle 中的细节如果某个拦截器的 preHandle 返回 falseSpring 会立刻停止调用后续拦截器并且触发已经通过的那些拦截器的 afterCompletion 方法。这保证了资源清理逻辑不会被跳过。interceptorIndex 用于记录当前执行到第几个拦截器afterCompletion 会从该下标往前倒序执行。5.2 哪些拦截器会作用于当前请求RequestMappingHandlerMapping 继承自 AbstractHandlerMapping后者内部维护了一个 adaptedInterceptors 列表。当 HandlerMapping 初始化时会收集两类拦截器一类是容器中所有实现了 HandlerInterceptor 接口的 Bean另一类是配置的 WebMvcConfigurer 里通过 addInterceptors 添加的拦截器。java// WebMvcConfigurer 中注册拦截器 Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/order/**) .excludePathPatterns(/order/login, /order/register); }在 getHandler 方法内部AbstractHandlerMapping 会遍历 adaptedInterceptors判断每个拦截器是否匹配当前请求路径。如果匹配就把它加入 HandlerExecutionChain。这里的映射也可以使用 MappedInterceptor 来声明匹配和不匹配路径。还需要注意 HandlerInterceptor 与 Filter 的区别。Filter 是 Servlet 规范的一部分由 Servlet 容器调度在进入 DispatcherServlet 之前执行HandlerInterceptor 是 Spring 框架层的概念在 DispatcherServlet 内部执行能拿到具体的 Handler 和 ModelAndView。Filter 适合做字符编码、登录校验等容器级处理拦截器适合做更细粒度的权限校验、日志记录、性能统计。六、HandlerAdapter适配器模式的具体应用找到 Handler 之后DispatcherServlet 不能直接调用它因为 Spring MVC 支持多种处理器类型标注 RequestMapping 的方法处理器 HandlerMethod、实现 Controller 接口的处理器、HttpRequestHandler、Servlet 处理器等。不同类型处理器调用方式不同于是 HandlerAdapter 登场。6.1 适配器模式HandlerAdapter 接口定义了两个关键方法supports 用于判断当前适配器是否支持某个 handlerhandle 则负责真正执行处理器并返回 ModelAndView。接口定义如下javapublic interface HandlerAdapter { boolean supports(Object handler); ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; }Spring MVC 内置了多个 HandlerAdapter其中最常用的有三个。RequestMappingHandlerAdapter 用于处理 HandlerMethod也就是由 RequestMapping 注解方法解析出来的处理器SimpleControllerHandlerAdapter 用于处理直接实现 Controller 接口的处理器HttpRequestHandlerAdapter 用于处理 HttpRequestHandler 类型的处理器。DispatcherServlet 在 getHandlerAdapter 阶段会遍历这些适配器调用 supports 方法找到第一个能处理当前 handler 的适配器。6.2 RequestMappingHandlerAdapter 如何调用处理器方法RequestMappingHandlerAdapter 是真正干重活的适配器。它拿到 HandlerMethod 之后会把参数解析、数据绑定、参数校验、返回值处理这一整套流程装配到 ServletInvocableHandlerMethod 上然后通过反射调用目标方法。简化后的调用骨架如下javaprotected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest new ServletWebRequest(request, response); WebDataBinderFactory binderFactory getDataBinderFactory(handlerMethod); ServletInvocableHandlerMethod invocableMethod createInvocableHandlerMethod(handlerMethod); invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.afterPropertiesSet(); ModelAndViewContainer mavContainer new ModelAndViewContainer(); invocableMethod.invokeAndHandle(webRequest, mavContainer); return getModelAndView(mavContainer, binderFactory, webRequest); }这段代码包含两个关键链路argumentResolvers 负责把方法参数从请求中一个一个解析出来returnValueHandlers 负责把方法返回值写回响应或者转换成 ModelAndView。后面两章会分别展开这两条链路。可以先记住参数解析和返回值处理都不是直接写在 Controller 里的而是由一组可插拔的解析器完成这正是 Spring MVC 能够支持 PathVariable、RequestParam、RequestBody、ResponseBody 等丰富注解的原因。七、参数解析HandlerMethodArgumentResolver 如何绑定参数当 Controller 方法声明了 PathVariable Long id、RequestParam String source、RequestBody QueryCondition condition 这样的参数时Spring 必须知道如何从 HttpServletRequest 中为每个参数找到值并转换成对应类型。这个任务由 HandlerMethodArgumentResolver 完成。7.1 参数解析器接口javapublic interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception; }supportsParameter 用来判断这个解析器能处理哪种参数resolveArgument 负责真正取值。Spring 把多个解析器放进一个 HandlerMethodArgumentResolverComposite 组合器中。在解析某个参数时组合器按顺序遍历所有解析器第一个 supportsParameter 返回 true 的解析器会被使用。常见的参数解析器有PathVariableMethodArgumentResolver 处理 PathVariableRequestParamMethodArgumentResolver 处理 RequestParamRequestResponseBodyMethodProcessor 处理 RequestBodyServletModelAttributeMethodProcessor 处理 ModelAttributeServletRequestMethodArgumentResolver 处理 HttpServletRequest 等。还有若干无注解参数的解析器比如默认模型属性绑定。7.2 PathVariable 路径变量的提取与绑定路由匹配阶段会使用 PathPattern 或 AntPathMatcher 对请求路径做 matchAndExtract得到一组路径变量保存在请求属性中。PathVariableMethodArgumentResolver 从这些属性里取出与方法参数名对应的值再通过 ConversionService 把字符串转换成 Long、Integer 等目标类型。javaGetMapping(/{id}) public ResultOrder getOrder(PathVariable(id) Long id) { orderService.findById(id); return Result.ok(); }如果路径变量名与方法参数名一致可以省略 PathVariable 的 value 属性但为了可读性建议显式写清楚。当方法参数类型是 MapString, String 时还能一次拿到全部路径变量不过一般业务代码不推荐这样写。7.3 RequestParam、RequestBody 与 ModelAttribute 的区别这三个注解经常被放在一起比较它们的数据来源完全不同。RequestParam从 URL 查询参数或表单参数中取值数据来自 request.getParameter适合简单的键值对场景。RequestBody读取整个请求体并通过 HttpMessageConverter 反序列化成对象最常见的是接收 application/json。ModelAttribute把多个请求参数绑定到一个 JavaBean 上既可以从表单绑定也可以绑定到 Model 中供视图使用。很多同学误以为 RequestParam 也能接 JSON这是不对的。JSON 请求体必须使用 RequestBody因为 JSON 不会被放进 getParameter 的参数表里。反过来RequestBody 也不能用于拼接在 URL 后面的 query string。7.4 Validated 参数校验与异常javaPostMapping(/create) public String create(Validated RequestBody CreateOrderRequest request) { orderService.create(request); return redirect:/order/ request.getId(); }当参数被 Validated 或 Valid 标注后参数解析完成、进入方法调用之前Spring 会使用 Hibernate Validator 等 JSR-303 实现执行校验。如果校验失败会抛出 MethodArgumentNotValidException对于 RequestBody 参数或 BindException对于模型绑定参数。这个异常最终会被异常解析器接住转换成 DTO 里的字段错误信息返回给客户端。八、HttpMessageConverter消息转换与 ResponseBodyHttpMessageConverter 是 Spring MVC 处理请求体和响应体的核心策略。它在参数解析和返回值处理中同时发挥作用RequestBody 靠它读请求体ResponseBody 靠它写响应体。javapublic interface HttpMessageConverterT { boolean canRead(Class? clazz, MediaType mediaType); boolean canWrite(Class? clazz, MediaType mediaType); ListMediaType getSupportedMediaTypes(); T read(Class? extends T clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; void write(T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; }canRead 和 canWrite 分别判断一个转换器能否读取或写出某个类型和媒体类型。read 负责把输入流转换成对象write 负责把对象写入输出流。Spring Boot 默认注册了一组消息转换器常见实现如下实现类支持的媒体类型典型用途MappingJackson2HttpMessageConverterapplication/jsonJSON 读写依赖 JacksonStringHttpMessageConvertertext/plain 等文本类型读写字符串ByteArrayHttpMessageConverterapplication/octet-stream读写字节数组FormHttpMessageConverterapplication/x-www-form-urlencoded处理表单与 MultiValueMapGsonHttpMessageConverterapplication/json使用 Gson 完成 JSON 转换8.1 消息转换器如何被选中以 RequestBody 为例RequestResponseBodyMethodProcessor 在读取参数时会拿到 Content-Type然后遍历所有 HttpMessageConverter找到第一个 canRead 返回 true 的转换器。Java 对象写回响应时同理Spring 会根据返回类型、Accept 头和要写出的对象类型找到合适的转换器进行序列化。如果客户端提交的 Content-Type 是 application/jsonSpring 会优先选择 MappingJackson2HttpMessageConverter如果没有匹配的转换器会抛出 HttpMediaTypeNotSupportedException 或 HttpMessageNotReadableException。8.2 为什么 ResponseBody 不经过 ViewResolver这要从返回值处理说起。HandlerMethodReturnValueHandler 处理返回值时先判断方法或类上是否标注了 ResponseBody。如果标注了RequestResponseBodyMethodProcessor 会直接用 HttpMessageConverter 把返回值写成响应体并调用 mavContainer.setRequestHandled(true)。这个方法一调用processDispatchResult 就知道请求已经处理完成不会再进入视图渲染阶段。因此ResponseBody 不经过 ViewResolver 并不是因为有个开关直接跳过视图而是因为返回值处理器在写响应体的同时把请求标记为“已处理”。而返回逻辑视图名的普通 Controller 方法不会设置这个标记后续才会走 ViewResolver。九、返回值处理与视图渲染9.1 HandlerMethodReturnValueHandlerjavapublic interface HandlerMethodReturnValueHandler { boolean supportsReturnType(MethodParameter returnType); void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception; }与参数解析器对称Spring 会维护一组 HandlerMethodReturnValueHandler。常见的实现有RequestResponseBodyMethodProcessor 处理 ResponseBody 返回值ViewNameMethodReturnValueHandler 处理 String 类型的视图名ModelAndViewMethodReturnValueHandler 处理 ModelAndView 返回值MapMethodProcessor 处理声明为视图页并在 Model 上放置数据的返回类型。9.2 视图解析与渲染如果返回值最终形成 ModelAndView 且视图名是逻辑视图名DispatcherServlet 会在 processDispatchResult 中调用 render 方法。render 内部先遍历 ViewResolver把逻辑视图名解析成真正的 View 对象再由 View 的 render 方法完成页面渲染。javaprotected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { View view; String viewName mv.getViewName(); if (viewName ! null) { view resolveViewName(viewName, mv.getModelInternal(), request); } else { view mv.getView(); } view.render(mv.getModelInternal(), request, response); }最典型的 InternalResourceViewResolver 会把视图名解析成 JSP 路径并通过请求转发交给 Servlet 容器。前后端分离项目中Controller 基本都返回 JSON视图渲染这条链路用得少但它同样是面试必考点。9.3 redirect 与 forward 前缀Controller 返回的字符串如果以 redirect: 开头Spring 会使用 RedirectView它向客户端发送 302 重定向浏览器会发起第二次请求。如果以 forward: 开头则使用 InternalResourceView 或其他支持转发的视图在服务端内部完成跳转。两者区别在于重定向会改变浏览器地址栏请求参数默认不保留转发地址栏不变是同一请求内部流转。十、异常处理HandlerExceptionResolver 与 ControllerAdviceController 中抛出的异常会在 processDispatchResult 中被捕获进而调用 processHandlerException。该方法遍历 HandlerExceptionResolver 列表找到能处理当前异常的解析器并返回 ModelAndView。javapublic interface HandlerExceptionResolver { ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); }10.1 默认异常解析器链Spring MVC 默认注册了三个异常解析器它们共同组成一条异常处理链。实现类作用ExceptionHandlerExceptionResolver处理标注 ExceptionHandler 的异常处理方法ResponseStatusExceptionResolver处理标注 ResponseStatus 的异常DefaultHandlerExceptionResolver把框架内部异常转换为标准 HTTP 状态码ExceptionHandlerExceptionResolver 的优先级最高通常由 ControllerAdvice 全局异常处理器来承接业务异常。DefaultHandlerExceptionResolver 负责兜底比如把 NoHandlerFoundException 转成 404把 HttpRequestMethodNotSupportedException 转成 405。10.2 ExceptionHandler 与 ControllerAdvicejavaRestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusinessException(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } ExceptionHandler(MethodArgumentNotValidException.class) public ResultVoid handleValidException(MethodArgumentNotValidException e) { String message e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(; )); return Result.fail(400, message); } }ExceptionHandler 可以写在 Controller 内部只处理当前 Controller 的异常也可以配合 ControllerAdvice 或 RestControllerAdvice 写全局处理器。RestControllerAdvice 相当于 ControllerAdvice 加 ResponseBody方法返回值会被直接序列化成 JSON。10.3 异常处理流程当 Controller 抛出异常后流程是这样的doDispatch 捕获异常并记录到 dispatchException随后进入 processDispatchResult。processDispatchResult 判断异常不为空调用 processHandlerException 遍历异常解析器。ExceptionHandlerExceptionResolver 找到匹配的 ExceptionHandler 方法后会像调用普通 Controller 方法一样经过参数解析、返回值处理最终生成 ModelAndView 或直接写出响应。如果异常解析成功Spring 会继续按正常流程渲染结果并执行拦截器的 afterCompletion。正是因为异常处理发生在拦截器后置方法 applyPostHandle 之后的 processDispatchResult 中所以 postHandle 在异常发生时不会被调用而 afterCompletion 始终会被调用。这是面试中一个很细但容易得分的问题。十一、完整调用时序图把前面的组件串起来可以得到下面这张完整时序图。它比单看 doDispatch 源码更直观推荐在面试前自己默画一遍十二、高频面试题与回答要点问题 1说说 Spring MVC 的执行流程。回答主线请求先到 DispatcherServletdoDispatch 中依次做 checkMultipart、getHandler、getHandlerAdapter、applyPreHandle、handle、applyPostHandle、processDispatchResult。其中 handle 内部完成参数解析、方法调用和返回值处理processDispatchResult 负责视图渲染或异常解析。问题 2HandlerMapping 和 HandlerAdapter 有什么区别HandlerMapping 负责“找”根据请求找到对应的 Handler 和拦截器链HandlerAdapter 负责“调”屏蔽不同类型 Handler 的调用差异用统一的方式执行处理器方法。问题 3ResponseBody 为什么不会走视图解析因为 RequestResponseBodyMethodProcessor 在写响应体的同时会调用 mavContainer.setRequestHandled(true)标记请求已处理完成processDispatchResult 检测到这个标记后就不再进入 render 阶段。问题 4参数是怎么绑定到 Controller 方法上的由 HandlerMethodArgumentResolver 完成。DispatcherServlet 委托 RequestMappingHandlerAdapter适配器持有一组解析器按 supportsParameter 匹配后调用 resolveArgument 取值并做类型转换。问题 5Filter 和 HandlerInterceptor 有什么区别Filter 属于 Servlet 规范由容器调度在 DispatcherServlet 之前执行能拿到 ServletRequest 但拿不到 HandlerHandlerInterceptor 属于 Spring 框架层在 DispatcherServlet 内部执行能拿到 HandlerMethod 和 ModelAndView更适合业务级拦截。问题 6异常是什么时候被处理的异常在 doDispatch 的 catch 块中记录到 dispatchException然后在 processDispatchResult 中触发 processHandlerException遍历 HandlerExceptionResolver 链。ExceptionHandler 优先级最高其次 ResponseStatus最后 DefaultHandlerExceptionResolver 兜底。问题 7postHandle 在异常情况下会被调用吗不会。异常处理发生在 applyPostHandle 之后的 processDispatchResult 中所以 postHandle 不会被调用但 afterCompletion 一定会被调用用于资源清理。十三、总结回到最初的问题Spring MVC 从请求到响应的执行过程。一个完整的回答应该覆盖四层入口层DispatcherServlet 通过 doDispatch 统一调度继承体系 HttpServletBean → FrameworkServlet → DispatcherServlet 各司其职。查找层HandlerMapping 基于 RequestMapping 收集映射运行时用 PathPattern 做最佳匹配返回 HandlerExecutionChain。调用层HandlerAdapter 驱动参数解析器和返回值处理器完成方法调用和结果处理HttpMessageConverter 负责请求体和响应体的序列化与反序列化。收尾层processDispatchResult 负责视图渲染和异常解析ViewResolver 和 HandlerExceptionResolver 各司其职拦截器的 afterCompletion 保证资源清理。
企业数字化 ERP 产品动态
相关推荐
前端路由与History API(进阶篇)最佳实践与踩坑记录 本文深入探讨前端路由与History API(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。在前端性能优化领域,前端路由与History API(进阶篇)是开发者和技术负责人持续关注的核心议… · 2026/11/3 4:44:00
跨域资源共享CORS详解(进阶篇):原理、方案与优化 本文深入探讨跨域资源共享CORS详解(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。 随着业务规模增长,跨域资源共享CORS详解(进阶篇)的重要性日益凸显。无论你是刚入门还是资深… · 2026/11/3 18:11:31
深入理解CSS性能优化与关键渲染路径(进阶篇) 本文深入探讨CSS性能优化与关键渲染路径(进阶篇),涵盖背景分析、原理剖析、实战步骤、配置示例、优化建议和避坑指南。 随着业务规模增长,CSS性能优化与关键渲染路径(进阶篇)的重要性日益凸显。无论你是刚入… · 2026/11/12 6:10:02
青山电工证在哪里办怎么考过不白交培训费 青山电工证在哪里办怎么考过不白交培训费 怕考不过白交培训费?别急,先搞清楚青山电工证在哪里办,电子证书怎么查,再一步步来准备,稳稳拿到证才是关键。 为什么你得知道青山电工证在哪里办… · 2026/11/13 11:53:00
应急办电工证查询怎么查?电子证书怎么拿?怕考不过白交培训费的看过来 应急办电工证查询怎么查?电子证书怎么拿?怕考不过白交培训费的看过来 你是不是也这样?交完培训费,心里直打鼓,怕考不过白搭钱,更怕以后干活没证干不了?别急,今天我就从洛阳在职建筑工人的角度,说说怎么查应急办电工证查询、怎么拿电子证书,还有那些… · 2026/11/13 11:53:00
建筑电工证考难不网上查询报考条件与考试时间 建筑电工证考难不网上查询报考条件与考试时间 你是不是正在找工作,听说没有建筑电工证根本进不了工地?别急,今天就给你讲讲 建筑电工证考难不 ,还有怎么查报名条件、考试时间,全是干货,看完你就知道该怎么准备了。… · 2026/11/13 11:53:00
工地太忙根本没时间复习考试?芦山县低压电工证考前押题帮你搞定 工地太忙根本没时间复习考试?芦山县低压电工证考前押题帮你搞定 工地太忙根本没时间复习考试,低压电工证考试内容又多又杂,眼看考试日期临近,连翻书的时间都没有,这事儿谁没经历过?特别是芦山县的电工朋友们,面对频繁的项目变更和工期压力,复习真的成… · 2026/11/13 11:53:00
高压电工作业证书复审怎么搞?考前押题资料真能救命 高压电工作业证书复审怎么搞?考前押题资料真能救命 干电工这行,没证就等于没饭碗。特别是高压电工作业证书,一旦过期不复审,不仅不能上岗,连面试资格都保不住。很多人为了能顺利复审,到处找 考前押题… · 2026/11/13 11:53:00
聊城电工证报名在哪?实操考试心里没底怕挂科?考前押题帮你稳过 聊城电工证报名在哪?实操考试心里没底怕挂科?考前押题帮你稳过 实操考试心里没底怕挂科?考前押题是关键。聊城电工证报名在哪?很多人在报名前都会遇到这样的问题。特别是初次报考的小伙伴,面对考试流程、报名地点、材料准备等,一时间不知从何下手。别担… · 2026/11/13 11:52:00
电工证丢失还能补吗?千万别踩坑,丢了证等于失业 电工证丢失还能补吗?千万别踩坑,丢了证等于失业 你正在找工作,但手里的电工证突然丢了,这下怎么办?别急,先别慌, 电工证丢失还能补吗 ?答案是肯定的,但 千万别踩坑 ,补办流程如果不了解,可能会耽误你上岗、甚至影响到你的收入。… · 2026/11/12 14:09:01
电力电缆作业证直播报名入口与办理流程良心建议 电力电缆作业证直播报名入口与办理流程良心建议 你是不是也担心学历不高报不了名?别急,今天就给你讲讲 电力电缆作业证直播 的报名流程和那些你必须知道的干货, 良心建议 在这里,别错过。 电力电缆作业证直播报名需要什么条件?… · 2026/11/13 5:40:45
英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 英德电工证报考费多少钱,怕考不过白交培训费?看完这篇别再踩坑 怕考不过白交培训费?英德电工证报考费多少钱,你真的了解吗?现在市面上培训机构五花八门,价格参差不齐,很多人一不小心就花了冤枉钱,结果还考不过。别急,今天就来给你说清楚英德电工证的… · 2026/11/12 23:27:43
维修电公用考电工证吗 在哪考最靠谱 维修电公用考电工证吗 在哪考最靠谱 工地太忙根本没时间复习考试,你是维修电工,天天在项目上跑,连抽个空背个题都难。那问题来了—— 维修电公用考电工证吗 ?答案是: 必须考 。尤其是在当前施工行业,没有电工证,连进场都可能被拦下。而且,… · 2026/11/13 0:00:08
电工证是不是可以登高?含金量和证书范围你必须知道的事 电工证是不是可以登高?含金量和证书范围你必须知道的事 证书过期了不知道怎么复审延期,这事儿很多人遇到过,尤其是像你这样在郑州金水区做劳务班组负责人,手里握着几十个工人的证书,稍微一疏忽,就可能影响工程进度和公司信誉。特别是电工证是不是可以登… · 2026/11/13 0:00:08
鸡西电工证哪家下证快官方报名入口怎么找 鸡西电工证哪家下证快官方报名入口怎么找 你是不是也担心 学历不高怕报不上名 ?别急,今天我就来跟你说说, 鸡西电工证哪家下证快 ,还有 官方报名入口 到底怎么找,别让那些“代办”“代考”耽误了你的机会。… · 2026/11/13 0:00:08
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/13 6:12:21
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/13 6:11:51
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/12 21:15:21