今天,想和你聊一個我們每天都在打交道,但可能不曾深入思考的話題:當一個 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 的請求處理有更深刻的理解,也是日常學習的一個記錄📝。