解密 Spring MVC:从 Tomcat 到 Controller 的一次完整请求之旅
今天,想和你聊一个我们每天都在打交道,但可能不曾深入思考的话题:当一个 HTTP 请求从浏览器发出,到最终被我们的 Spring Controller 处理,它到底经历了一场怎样的旅程?
理解这个流程,不仅仅是为了应付面试,更是为了在遇到棘手问题时,能像庖丁解牛一样,精准定位问题所在。这趟旅程,我们可以清晰地划分为两大站:Tomcat 处理阶段和 Spring MVC 处理阶段。
第一站:Tomcat 的守门与引导
在请求进入 Spring 的世界之前,Tomcat 作为"前哨站",需要完成一系列的接待和引导工作。
1. 门口的接待员:Connector
当一个请求,比如 http://localhost:8080/user/info
,敲响 8080 端口的大门时,Tomcat 的 Connector 组件第一个站出来迎接。它的职责就是监听网络端口,接收原始的 TCP 连接,并将其解析成一个标准的 HttpServletRequest
对象。
同时,为了高效处理并发,Tomcat 会从一个线程池(比如 http-nio-8080-exec-1
)中取出一个工作线程,专门为这个请求服务,直到响应完成。
2. 容器的层层路由
请求对象创建好后,就进入了 Tomcat 的容器内部。这个过程就像一个俄罗斯套娃,请求会依次经过 Engine
→ Host
→ Context
→ Wrapper
这几层。
// 伪代码:感受一下这个调用链
connector.getService().getContainer().getPipeline().getFirst().invoke(request, response);
- Engine: 全局引擎,服务于整个 Tomcat 实例。
- Host: 虚拟主机,对应一个域名,比如
localhost
。 - Context: Web 应用,对应我们的项目。Tomcat 在这里根据
/
之后的 URL 路径,匹配到处理该路径的Context
。 - Wrapper: Servlet 包装器。最终,
Context
会根据web.xml
中配置的servlet-mapping
,找到处理这个请求的最终 Servlet。在 Spring Boot 应用中,这个 Servlet 通常就是大名鼎鼎的DispatcherServlet
。
3. 第一道安检:过滤器链 (Filter Chain)
在请求被正式交给 DispatcherServlet
之前,它必须先通过一系列"安检"——这就是过滤器(Filter)。
过滤器是 Servlet 规范的一部分,像一道道关卡,按顺序执行。
// 过滤器链执行伪代码
// 只有当所有 Filter 都放行,请求才会最终到达 Servlet
for (Filter filter : filters) {filter.doFilter(request, response, chain);
}
// 链的末端,触发 Servlet 的 service 方法
chain.doFilter(request, response);
实战场景:
CharacterEncodingFilter
: 确保全站请求和响应的字符集统一,防止乱码。CorsFilter
: 解决跨域问题,允许特定来源的前端应用访问。- 自定义的
JwtAuthFilter
: 对受保护的 API 进行身份验证,解析 Token,并将用户信息存入SecurityContext
。 LoggingFilter
: 记录所有请求的详细日志,便于审计和调试。
只有通过了所有过滤器的"盘问",请求才算完成了在 Tomcat 阶段的旅程,正式敲响了 Spring MVC 的大门。
第二站:Spring MVC 的调度中心 - DispatcherServlet
DispatcherServlet
是 Spring MVC 的绝对核心,堪称"中央调度员"。它接管请求后,会在其 doDispatch
方法内, orchestrate(精心安排)后续所有操作。
// DispatcherServlet.doDispatch 精简核心逻辑
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {// 1. 根据请求查找 Handler(即 Controller 方法)HandlerExecutionChain mappedHandler = getHandler(request);// 2. 获取能执行这个 Handler 的适配器 HandlerAdapterHandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());// 3. 执行拦截器的 preHandle() 方法,这是进入 Controller 前的最后一道关卡if (!mappedHandler.applyPreHandle(request, response)) {return; // 如果 preHandle 返回 false,请求被中断}// 4. 真正调用 Controller 方法ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());// 5. 执行拦截器的 postHandle() 方法mappedHandler.applyPostHandle(request, response, mv);// 6. 处理派发结果(如渲染视图或处理异常)processDispatchResult(request, response, mappedHandler, mv, null);
}
让我们一步步拆解这个过程:
1. HandlerMapping:找到对的人
DispatcherServlet
首先会询问 HandlerMapping
:“嘿,这个 URL (/user/info
) 应该由哪个 Controller 的哪个方法来处理?”。
RequestMappingHandlerMapping
会扫描所有被 @RequestMapping
、@GetMapping
等注解标记的方法,构建一个 URL 与 HandlerMethod
的映射关系,然后精准地找到匹配项。
2. Interceptor preHandle:Controller 前的最后机会
找到目标 Controller
方法后,并不会立刻执行。而是先执行所有匹配该路径的**拦截器(Interceptor)**的 preHandle
方法。
这是一个关键的切入点。preHandle
返回 true
则放行,返回 false
则请求被直接中断。
// 拦截器 preHandle 示例
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {// 比如,进行更细粒度的权限校验if (!checkAuth(request, handler)) { // 甚至可以拿到 handler 信息做更复杂的判断response.sendError(403, "权限不足");return false; // 中断请求}return true; // 放行
}
3. 参数解析与 Controller 方法执行
通过了所有拦截器的 preHandle
后,HandlerAdapter
开始工作。它会借助 HandlerMethodArgumentResolver
等一系列"参数解析器",神奇地将 HTTP 请求中的各种信息(如 @RequestBody
的 JSON、@RequestParam
的查询参数、@PathVariable
的路径变量)转换并注入到你 Controller
方法的参数列表中。
然后,通过反射,你的 Controller
方法终于被执行了!
4. AOP 切面:无感知的逻辑增强
就在你的 Controller
方法执行前后,AOP(面向切面编程)可能会"神不知鬼不觉"地介入。如果你的方法上加了 @Transactional
、@Cacheable
或是自定义的 AOP 注解,那么相关的切面逻辑(如环绕通知)会在这里执行。
// 环绕通知示例:在 Controller 方法执行前后织入逻辑
@Around("@annotation(com.example.MyCustomLog)")
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {long start = System.currentTimeMillis();// 执行 Controller 方法Object result = joinPoint.proceed();long duration = System.currentTimeMillis() - start;log.info("{} 执行耗时: {} ms", joinPoint.getSignature(), duration);return result;
}
AOP 的美妙之处在于,它让你的业务代码保持纯净,同时又能附加额外的通用功能。
5. Interceptor postHandle & 视图渲染
Controller 方法执行完毕,并返回了一个结果(比如一个 ModelAndView
对象或者一个被 @ResponseBody
标记的对象)。
此时,拦截器的 postHandle
方法会被调用。你可以在这里对 ModelAndView
进行修改,或者在响应提交前做一些额外操作。
如果返回的是 ModelAndView
,DispatcherServlet
会通过 ViewResolver
(视图解析器)找到对应的视图模板(如 Thymeleaf 或 JSP),并用模型数据进行渲染,最终生成 HTML 响应。
6. Interceptor afterCompletion:最后的清理工作
无论请求处理过程中是否发生异常,只要它经过了拦截器的 preHandle
并返回 true
,那么在整个请求完成(视图渲染完毕或响应已提交)后,拦截器的 afterCompletion
方法就一定会被调用。
这里是执行资源清理工作的最佳地点,比如清理线程绑定的变量等。
全景图:一张图看懂执行顺序
为了更直观地理解整个流程,我为你绘制了一张流程图:
调试技巧:在 DispatcherServlet
的 doDispatch
方法里打上一个断点,然后单步调试。你会清晰地看到 getHandler
、applyPreHandle
、ha.handle
等关键步骤的调用过程,这是理解整个流程最快的方式。
实战排雷:常见问题与调试技巧
1. 灵魂拷问:Filter vs. Interceptor?
这是个老生常谈但至关重要的问题。
特性 | 过滤器 (Filter) | 拦截器 (Interceptor) |
---|---|---|
出身 | Servlet 规范,J2EE 标准,任何 Web 框架都能用 | Spring MVC 框架特有,高度集成于 Spring 上下文 |
执行时机 | 在 DispatcherServlet 之前,无法触及 Controller | 在 DispatcherServlet 之后,Controller 执行前后 |
依赖注入 | 默认不支持 @Autowired ,需特殊配置(如 FilterRegistrationBean ) | 由 Spring IoC 容器管理,可直接 @Autowired 注入任何 Bean |
能力范围 | 能处理所有进入 Tomcat 的请求,包括静态资源 | 只能拦截进入 DispatcherServlet 的请求 |
获取信息 | 无法直接获取即将执行的 Controller 方法信息 | 可以获取 HandlerMethod ,知道具体是哪个方法在处理 |
一句话总结:Filter
是粗粒度的全局"门卫",适合做认证、编码等通用工作;Interceptor
是细粒度的"警卫",适合做权限、日志等与业务逻辑相关的校验。
2. 为何静态资源不经过我的拦截器?
因为 Spring Boot 默认配置下,对于 /static
、/public
等目录下的静态资源请求,会由一个名为 DefaultServletHttpRequestHandler
的处理器直接处理,它会绕过 DispatcherServlet
,直接将资源以流的形式返回。因此,你的拦截器自然也就不会被触发。
3. 如何优雅地跳过某些路径的拦截器?
在配置拦截器时,使用 excludePathPatterns
方法。
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new MyAuthInterceptor()).addPathPatterns("/**") // 拦截所有.excludePathPatterns("/login", "/error", "/static/**"); // 排除特定路径}
}
4. 如何捕获全局异常?
使用 @ControllerAdvice
和 @ExceptionHandler
的组合拳,可以优雅地处理全局异常,避免 try-catch
遍地开花。
@ControllerAdvice
public class GlobalExceptionHandler {@ExceptionHandler(Exception.class)public ResponseEntity<String> handleGenericException(Exception e) {// 记录日志log.error("系统发生未知异常", e);// 返回一个对用户友好的错误信息return ResponseEntity.status(500).body("服务器开小差了,请稍后再试~");}@ExceptionHandler(IllegalArgumentException.class)public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {return ResponseEntity.status(400).body("请求参数不合法: " + e.getMessage());}
}
总结:
好了,这次从 Tomcat 到 Controller 的请求之旅就到这里。我们一起梳理了其中的每一个关键节点和核心组件。
掌握这条核心路径,你就能:
- 清晰定位问题:到底是 Filter 拦了,还是 Interceptor 没过?是参数解析错了,还是 AOP 出了异常?
- 优雅设计系统:合理地在 Filter、Interceptor、AOP、ControllerAdvice 中放置你的逻辑,让代码结构更清晰,职责更分明。
- 提升性能:理解了流程,才能更好地进行性能分析和优化。
这次的深度剖析,能让我们都对 Spring MVC 的请求处理有更深刻的理解,也是日常学习的一个记录📝。