SpringMVC HandlerInterceptor詭異問題排查

發現問題

最近在進行壓測發現,有一些接口時好時壞,經過sentry日誌平臺及sky walking平臺跟蹤發現,用戶張三獲取到的用戶上下文確是李四。spring

代碼走讀

用戶登陸下上文json

/**
 * 用戶登陸下上文
 *
 * @author : jamesfu
 * @date : 22/5/2019
 * @time : 9:18 AM
 */
@Data
public class UserContext {
    private final static ThreadLocal<UserContext> threadLocal = new ThreadLocal<>();

    private Long id;

    private String loginName;

    public static UserContext get() {
        UserContext context = threadLocal.get();
        if (context == null) {
            // TODO(james.h.fu):根據請求上下文獲取token, 而後恢復用戶登陸下上文
            context = new UserContext() {{
                setId(1L);
                setLoginName("james.h.fu1");
            }};
            threadLocal.set(context);
        }

        return context;
    }

    public static void clear() {
        threadLocal.remove();
    }

    public static void set(UserContext context) {
        if (context != null) {
            threadLocal.set(context);
        }
    }
}
複製代碼

在攔截器中有調用UserContext.set恢復用戶登陸上下文,並在請求結束時調用UserContext.clear清理用戶登陸上下文。 tomcat

攔截器在preHandle中根據請求上下文恢復登陸信息

攔截器註冊配置bash

/**
 * 攔截器註冊配置
 *
 * @author : jamesfu
 * @date : 22/5/2019
 * @time : 9:15 AM
 */
@Configuration
public class FilterConfig implements WebMvcConfigurer {
    @Autowired
    private JsonRpcInterceptor jsonRpcInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jsonRpcInterceptor)
                .addPathPatterns("/json.rpc");
    }
}
複製代碼

調試過程當中探測springmvc已註冊的攔截器有四個

遇到preHandle返回false就中斷調用鏈條,開始執行afterCompletion

調試日誌也驗證了確實沒有執行afterCompletion中的UserContext.clear

經過debug能夠發現UserContext中的ThreadLocal的清理工做沒有獲得執行。致使請求進來時,有可能ThreadLocal已存在了,就不會再根據請求上下文恢復了。架構

springmvc 源碼走讀

tomcat 在收到http請求後,最終會交由spring mvc的DispatcherServlet處理。 這裏能夠從doDispatch按圖索驥,順藤摸瓜地往下看起走。mvc

源碼走讀:DispatcherServlet

/**
	 * Process the actual dispatching to the handler.
	 * <p>The handler will be obtained by applying the servlet's HandlerMappings in order. * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
	 * to find the first that supports the handler class.
	 * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers * themselves to decide which methods are acceptable. * @param request current HTTP request * @param response current HTTP response * @throws Exception in case of any kind of processing failure */ protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception 複製代碼

請求會獲得分發,而後執行各個已註冊Handler的preHandle-->postHandle-->afterCompletion。app

源碼走讀:HandlerExecutionChain

applyPreHandle
/**
	 * Apply preHandle methods of registered interceptors.
	 * @return {@code true} if the execution chain should proceed with the
	 * next interceptor or the handler itself. Else, DispatcherServlet assumes
	 * that this interceptor has already dealt with the response itself.
	 */
	boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HandlerInterceptor[] interceptors = getInterceptors();
		if (!ObjectUtils.isEmpty(interceptors)) {
			for (int i = 0; i < interceptors.length; i++) {
				HandlerInterceptor interceptor = interceptors[i];
				if (!interceptor.preHandle(request, response, this.handler)) {
					triggerAfterCompletion(request, response, null);
					return false;
				}
				this.interceptorIndex = i;
			}
		}
		return true;
	}
複製代碼

當執行到preHandle返回false時,它就會從上一個返回true的handler依次往前執行afterCompletion,它本身的afterCompletion得不到執行。分佈式

triggerAfterCompletion
/**
	 * Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
	 * Will just invoke afterCompletion for all interceptors whose preHandle invocation
	 * has successfully completed and returned true.
	 */
	void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
			throws Exception {

		HandlerInterceptor[] interceptors = getInterceptors();
		if (!ObjectUtils.isEmpty(interceptors)) {
			for (int i = this.interceptorIndex; i >= 0; i--) {
				HandlerInterceptor interceptor = interceptors[i];
				try {
					interceptor.afterCompletion(request, response, this.handler, ex);
				}
				catch (Throwable ex2) {
					logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
				}
			}
		}
	}
複製代碼

triggerAfterCompletion只會在(1)出現異常,(2)preHandle返回false 或(3)正常執行結束纔會從索引interceptorIndex依次往前執行。ide

因此基於以上源碼能夠得知,在寫攔截器時preHandle返回false時,afterCompletion是不會執行的。因此一些必要的清理工做得不到執行,會出現相似咱們遇到的賬號串的問題。post

參考資料

關注公衆號交流學習

關注Java分佈式架構實戰, 持續精進

聯繫我,學習交流
相關文章
相關標籤/搜索