浅谈SpringMVC HandlerInterceptor诡异问题排查
发现问题
最近在进行压测发现,有一些接口时好时坏,通过sentry日志平台及sky walking平台跟踪发现,用户张三获取到的用户上下文确是李四。
代码走读
用户登录下上文
/** * 用户登录下上文 * * @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清理用户登录上下文。
拦截器注册配置
/** * 拦截器注册配置 * * @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"); } }
通过debug可以发现usercontext中的threadlocal的清理工作没有得到执行。导致请求进来时,有可能threadlocal已存在了,就不会再根据请求上下文恢复了。
springmvc 源码走读
tomcat 在收到http请求后,最终会交由spring mvc的 dispatcherservlet
处理。 这里可以从dodispatch按图索骥,顺藤摸瓜地往下看起走。
源码走读: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。
源码走读: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依次往前执行。
所以基于以上源码可以得知,在写拦截器时prehandle返回false时,aftercompletion是不会执行的。所以一些必要的清理工作得不到执行,会出现类似我们遇到的帐号串的问题。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。