> 本文以一個通過正常註冊攔截器流程註冊攔截器失敗的實際場景,來帶領大家閱讀源碼,體會Spring的HandlerInterceptor攔截器整個工作流程 ### 簡單認識 org.springframework.web.servlet.HandlerInterceptor是Spring框架中的一個 ...
本文以一個通過正常註冊攔截器流程註冊攔截器失敗的實際場景,來帶領大家閱讀源碼,體會Spring的HandlerInterceptor攔截器整個工作流程
簡單認識
org.springframework.web.servlet.HandlerInterceptor是Spring框架中的一個介面,用於攔截處理程式(Handler)的請求和響應。它允許開發人員在請求處理程式執行之前和之後執行自定義的預處理和後處理邏輯。 HandlerInterceptor介面定義了三個方法:
- preHandle:在請求處理程式執行之前調用。可以用於進行許可權驗證、日誌記錄等操作。如果該方法返回false,則請求將被中斷,後續的攔截器和處理程式將不會被執行。
- postHandle:在請求處理程式執行之後、視圖渲染之前調用。可以對請求的結果進行修改或添加額外的模型數據。
- afterCompletion:在整個請求完成之後調用,包括視圖渲染完畢。可用於進行資源清理等操作。 通過實現HandlerInterceptor介面,可以自定義攔截器,並將其註冊到Spring MVC的配置中。攔截器可以攔截指定的URL或者所有請求,併在請求的不同階段執行相應的邏輯。
正常的攔截器註冊流程
定義一個攔截器,實現org.springframework.web.servlet.HandlerInterceptor介面
如:
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// ...
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
// ...
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
// ...
}
}
定義配置類,實現WebMvcConfigurer,實現addInterceptors進行攔截器註冊
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// ...
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns(HOTSWAP_ALL_SERVICE)
.addPathPatterns(hsAddPaths)
.excludePathPatterns(commonExcludePaths);
// ...
}
}
而有時候通過這種方式註冊不成功。
我們先來說一下說一下HandlerInterceptor的工作機制。首先以上描述的是攔截器的註冊過程。
然後再來看一下註冊的攔截器存儲在哪裡,以及如何被使用的。
存儲已註冊攔截器的位置
查看介面方法org.springframework.web.servlet.config.annotation.WebMvcConfigurer#addInterceptors
的調用之處,位於類org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration
裡面,其是WebMvcConfigurationSupport
的子類
WebMvcConfigurationSupport的一個子類,它檢測並委托給所有WebMvcConfigurer類型的bean,允許它們自定義WebMvcConfigurationSupport提供的配置。這是@EnableWebMvc實際導入的類。
org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors
方法:
@Override
protected void addInterceptors(InterceptorRegistry registry) {
this.configurers.addInterceptors(registry);
}
可以看出,這是從我們註冊的WebMvcConfigurer實現類中,通過傳入的InterceptorRegistry實例搜集攔截器,而調用該方法的地方位於
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping
,實際是其預設子類DelegatingWebMvcConfiguration
進行註冊RequestMappingHandlerMapping bean 的行為,在其方法中調用了org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors
方法,解析InterceptorRegistry裡面添加的攔截器。
org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#getInterceptors
* Provide access to the shared handler interceptors used to configure
* {@link HandlerMapping} instances with.
* <p>This method cannot be overridden; use {@link #addInterceptors} instead.
*/
protected final Object[] getInterceptors(
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
if (this.interceptors == null) {
InterceptorRegistry registry = new InterceptorRegistry();
addInterceptors(registry);
registry.addInterceptor(new ConversionServiceExposingInterceptor(mvcConversionService));
registry.addInterceptor(new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider));
// 排序
this.interceptors = registry.getInterceptors();
}
return this.interceptors.toArray();
}
整個過程就是,WebMvcConfigurationSupport在註冊RequestMappingHandlerMapping時候,會創建一個InterceptorRegistry實例, 被傳入了org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration#addInterceptors
方法,進而傳入了org.springframework.web.servlet.config.annotation.WebMvcConfigurerComposite#addInterceptors
方法,最後被每個WebMvcConfigurer的實現類的addInterceptors方法所接收,用於搜集用戶自定義的攔截器註冊。即幫助配置映射攔截器列表。
註冊失敗原因
我們來看一下下麵這個註冊失敗的場景:
@Configuration
@ConditionalOnProperty(name = "spring.mvc.configuration.custom", havingValue = "false", matchIfMissing = true)
public class CustomSpringMvcConfiguration extends WebMvcConfigurationSupport {
// ...
}
原因是一個公共的jar包註冊了一個的子類,導致預設的註冊子類DelegatingWebMvcConfiguration沒有被註冊,而其對方法的重寫,也沒有覆寫DelegatingWebMvcConfiguration的預設實現。
@Override
protected void addInterceptors(InterceptorRegistry registry) {
if (auditTrailLogInterceptor != null) {
registry.addInterceptor(auditTrailLogInterceptor).addPathPatterns("/**");
log.info("已註冊 AuditTrailLog 攔截器");
}
if (openApiLogInterceptor != null) {
registry.addInterceptor(openApiLogInterceptor).addPathPatterns("/**");
log.info("已註冊 openApiLog 攔截器");
}
super.addInterceptors(registry);
}
其中
super.addInterceptors(registry);
是個無效的語句,因為父類是一個空實現。相當於文首的註冊攔截器方法被堵死了,迫於無法直接修改jar包的情況,我們只能另尋方法。
初步嘗試
由於所有註冊的攔截器,最終都是放在org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors
中,所以我們要想辦法拿到AbstractHandlerMapping的註冊bean,即上面提到的org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerMapping
方法註冊的bean,然後將我們的攔截器註冊並放置進去。
既然上述註冊方法行不通,那麼我們的配置類就無需實現WebMvcConfigurer介面了。
我們需要重寫一個配置類,註入RequestMappingHandlerMapping對象,然後進行手動註冊。直接在構造函數中註冊。
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration {
public WebMvcConfiguration(
@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping) {
// 反射拿到已經註冊的攔截器集合
List<Object> interceptors = ReflectionUtil.getField(mapping, "interceptors");
InterceptorRegistry registry = new InterceptorRegistry();
registry.addInterceptor(new LoginInterceptor()).addPathPatterns(urlPatterns);
List<Object> list = ReflectionUtil.invoke(registry, "getInterceptors", null, null);
interceptors.addAll(list);
}
}
先通過反射拿到interceptors變數,然後new一個InterceptorRegistry,將其添加進去後反射調用其getInterceptors,並添加到RequestMappingHandlerMapping的變數interceptors當中去。
這似乎完成了註冊,萬事大吉了,但當我啟動項目,發現還想沒有註冊成功。這就令我費解了。到底是哪裡出了問題?
攔截器的使用
後面經過大神的指點,可以從以下思路尋找答案:
因為我這個攔截器攔截了部分路徑而已,之所以說沒註冊成功,是因為攔截的這些路徑沒有被攔截到。那既然存放的地方已經解決了,那問題就應該從使用的地方尋找答案。
這些攔截器,都帶著器攔截的路徑信息,而sping的請求,都會進入一個Servlet,因此這些存儲的攔截器,必定是在這個Servlet中使用,因為Servlet能拿到請求的HttpServletRequest信息,請求的路徑正是從這裡拿到。
這個Servlet,就是DispatcherServlet。
我們要查看DispatcherServlet是在哪裡使用到這些攔截器的
在org.springframework.web.servlet.DispatcherServlet#doService
中,會調用org.springframework.web.servlet.DispatcherServlet#doDispatch
方法
/** 處理對處理程式的實際調度。 處理程式將通過按順序應用servlet的HandlerMappings來獲得。HandlerAdapter將通過查詢servlet已安裝的HandlerAdapter來獲得,以找到第一個支持處理程式類的HandlerAdapter。 所有HTTP方法都由這個方法處理。由HandlerAdapters或處理程式本身來決定哪些方法是可接受的。
參數:
request -當前HTTP請求
response -當前HTTP響應
拋出:
異常——在任何處理失敗的情況下 */
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);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
其中的
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
就是確定這次請求的所有的匹配的HandlerInterceptor,也就是說,這裡會從RequestMappingHandlerMapping對象獲取所有註冊的HandlerInterceptor,然後根據request的路徑匹配,覺得哪些攔截器是該攔截該請求的,並根據順序形成攔截鏈。
這一關鍵操作的位置,位於org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandlerExecutionChain
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request, LOOKUP_PATH);
// 遍歷所有攔截器,進行匹配,匹配上的假如攔截鏈
for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
if (interceptor instanceof MappedInterceptor) {
MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
chain.addInterceptor(mappedInterceptor.getInterceptor());
}
}
else {
chain.addInterceptor(interceptor);
}
}
return chain;
}
以上代碼大概的流程就是渠道所有的攔截器,和取到的請求路徑進行正則匹配,匹配上的話,就添加進入攔截鏈
org.springframework.web.servlet.handler.MappedInterceptor#matches
用到的工具類是org.springframework.util.AntPathMatcher,PathMatcher的預設實現類
ant風格路徑模式的PathMatcher實現。
部分映射代碼是從Apache Ant中借來的。
該映射使用以下規則匹配url:
? 匹配一個字元
*匹配零個或多個字元
** 匹配路徑中的零個或多個目錄
{spring:[a-z]+}匹配regexp [a-z]+作為名為"spring"的路徑變數
例子
com/t?st .jsp -匹配com/test.jsp,也匹配com/ taste .jsp或com/txst.jsp .jsp com/t?st.jsp — matches com/test.jsp but also com/tast.jsp or com/txst.jsp
com/.jsp — 匹配所有在com文件夾中的 .jsp
com/ * * /test.jsp — 匹配com路徑下的所有 test.jsp文件
org/springframework/ * * /*.jsp —匹配org/springframework路徑下的所有.jspcom/{filename:\w+}.jsp 匹配com/test.jsp 並將值 test 賦值給 filename 變數
註意:模式和路徑必須都是絕對的,或者都是相對的,以便兩者匹配。因此,建議使用此實現的用戶對模式進行消毒,以便在使用模式的上下文中使用“/”作為首碼。
重點來了,細心的朋友會發現, 其遍歷的攔截器列表,是org.springframework.web.servlet.handler.AbstractHandlerMapping#adaptedInterceptors
,不是我們上面提及的org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors
,這也就解釋了為啥我們的攔截器好像沒註冊成功的原因,原來還差一步沒銜接上。
因此我們要找一下org.springframework.web.servlet.handler.AbstractHandlerMapping#interceptors
是如何轉換成org.springframework.web.servlet.handler.AbstractHandlerMapping#adaptedInterceptors
的
通過查看的其被調用add的方法,可以找到下麵轉換方法
org.springframework.web.servlet.handler.AbstractHandlerMapping#initInterceptors
protected void initInterceptors() {
if (!this.interceptors.isEmpty()) {
for (int i = 0; i < this.interceptors.size(); i++) {
Object interceptor = this.interceptors.get(i);
if (interceptor == null) {
throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
}
this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
其轉換的時機是在應用上下文初始化成功後通知的時候
org.springframework.context.support.ApplicationObjectSupport#setApplicationContext
ApplicationObjectSupport實現了ApplicationContextAware介面
最終實現
至此攔截器的整個工作流程正式完成了閉環。由於initInterceptors是被保護的方法,我們同樣需要解決反射工具來完成調用。因此最終的攔截器註冊配置類實現如下
@Configuration(proxyBeanMethods = false)
public class WebMvcConfiguration {
public WebMvcConfiguration(
@Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping mapping,
Environment environment) {
// 從配置文件中讀取攔截路徑的配置
String patternConfig = environment.getProperty("app.config.client-filter-url-patterns");
List<String> urlPatterns = new ArrayList<>();
if (StringUtils.hasText(patternConfig)) {
urlPatterns = Arrays.asList(patternConfig.split(SymbolConsts.COMMA));
}
// 反射拿到已經註冊的攔截器存放點
List<Object> interceptors = ReflectionUtil.getField(mapping, "interceptors");
InterceptorRegistry registry = new InterceptorRegistry();
registry.addInterceptor(new LoginInterceptor()).addPathPatterns(urlPatterns);
List<Object> list = ReflectionUtil.invoke(registry, "getInterceptors", null, null);
// 添加進去
interceptors.addAll(list);
// 防止有已經轉換完成的攔截器被覆蓋或者衝突,先清空已經init的adaptedInterceptor
List<HandlerInterceptor> adaptedInterceptors = ReflectionUtil.getField(mapping, "adaptedInterceptors");
adaptedInterceptors.clear();
// 重新init
ReflectionUtil.invoke(mapping, "initInterceptors", null, null);
}
}