一、缘起
某天测试环境更新后,有小伙伴反应页面会随机性的发生请求参数为空的情况(request.getParamter为空),但是前端的参数是传了的,而且不能稳定重现,需要在页面上经过一番操作之后才会发生,而当问题重现之后,之前那些可用的页面就变得不可用了,然后就会在可用和不可用之间交替......
我接到问题的第一反应是
二、踩坑
2.1 寻找罪魁祸首
代码中request为空,但是前端有传递,第一时间想到的就是线程切换导致ThreadLocal传递出现问题。
然而这个坑我们之前是踩过的,并且已经在切面中手动改成了可继承的线程变量
HttpServletRequest servletRequest = WebUtil.getRequest();
HttpServletResponse servletResponse = WebUtil.getResponse();
//声明子线程的时候,这些属性不会继承,手动赋值成可继承的属性
ServletRequestAttributes attributes = new ServletRequestAttributes(servletRequest, servletResponse);
RequestContextHolder.setRequestAttributes(attributes, true);
LocaleContextHolder.setLocaleContext(LocaleContextHolder.getLocaleContext(), true);
难道切面没生效?
可是经过调试发现,这段代码是进入并执行了的。
通过查看提交记录发现,切面中有人加了这么一段代码(没错就是我)
ExecutorService TIMEOUT_EXECUTOR_POOL = new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() + 1, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
new SynchronousQueue<>(), ThreadUtil.newNamedThreadFactory("TIMEOUT_EXECUTOR_POOL", false)
);
FutureTask<Object> futureTask = new FutureTask<>(() -> {
try {
return joinPoint.proceed();
} catch (Exception ex) {
throw ex;
} catch (Throwable throwable) {
throw new Exception(throwable);
}
});
TIMEOUT_EXECUTOR_POOL.submit(futureTask);
为了增加超时时间的控制,我用FutureTask把执行的代码包装了一层
在这里打断点调试,发现在报错的时候,futureTask外部request参数有值,进入后参数为空。
但是,偶尔也是会有值的!有值的时候就是页面正常的时候。
2.2 找出作案动机(原因)
我们先看下InheritableThreadLocal是怎么实现线程变量可继承的
在Thread的init()方法中有一段代码
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
//省略部分代码
//如果父线程inheritableThreadLocals不为空,则保存下来
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//省略部分代码
}
可以看到InheritableThreadLocal是在Thread创建的时候继承的。
而我们知道线程池的作用就是“缓存”线程来避免线程频繁的创建和销毁,所以如果在线程池中使用InheritableThreadLocal,只有第一个创建线程时的请求是可以用的,后续请求的InheritableThreadLocal都跟第一个请求一样,不会再改变。
至此,问题原因找到了,因为我创建线程池的时候初始化了CPU核数+1个线程,所以开始一些请求是正常的,后续当这些线程都使用了之后,就会因为InheritableThreadLocal不同导致错误。而且我们自己测试的时候是在几个按钮中重复点击,如果线程的第一个请求是/user/query,当你再次发起这个请求的时候如果刚好分配的是这个线程,页面就是正常的,于是就出现页面时好时坏的情况.
三、填坑
OK,出现问题的地方找到了,下面来解决
1、直接注释掉这段超时控制的代码
这个实在是太粗暴了,只适合紧急情况下使用,作为一个有追求的程序猿,我是不可能这么做的
2、不用线程池,直接new Thread
既然是线程池复用导致的问题,不用线程池就可以解决
3、使用阿里的TransmittableThreadLocal
https://github.com/alibaba/transmittable-thread-local
阿里巴巴开源了一个类似于InheritableThreadLocal的库,就是用来在线程池中使用,有兴趣的可以瞅一眼
来源:oschina
链接:https://my.oschina.net/u/2745108/blog/3116131