前言:
最近遇到一个问题,就是在做公司一些组件的时候,会将一些信息放置在请求头里,然后在不同的组件中获取实现不同的业务逻辑,但是在其他产品线接入的时候发现,当使用异步线程的时候,在线程任务中使用组件,request信息会丢失,导致获取的信息是空的导致结果不正确,这里记录下解决的过程。
1,情况模拟
由于中间件和业务代码过于复杂,就不完全复现了,这里就简单演示下
①,线程池定义
@Configuration
public class ThreadPoolConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(){
return new ThreadPoolExecutor(2, 2, 10L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(30));
}
}
②,使用异步线程获取request信息
@RestController
@RequestMapping(value = "/api/request")
@RequiredArgsConstructor
public class TestController {
private final ThreadPoolExecutor threadPoolExecutor;
@GetMapping(value = "/thread")
public String testThread(){
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
String authorization = request.getHeader("Authorization");
System.out.println("请求头信息为:"+ authorization);
}
});
return "success";
}
}
③,测试 简单定义两个请求,request.header里的Authorization分别对应不同值
④,请求结果
请求正常返回了,但是异步线程中却报了这样一个错误,大概意思就是在当前线程中没有找到request,虽然我在中间件中有判断此情况,但是获取不到request,业务肯定是不正确的。
2,问题分析
的确,request是当前请求的线程生成的,属于当前请求线程的变量,在异步线程中尝试获取,是获取不到的,所以这里报了这个错误。
3,解决过程
①,首先我尝试将request变量直接透传到子线程中,刚开始没啥问题,但是多测试了几下,就发现request获取的值是不对的,已经混乱了。经过多放查找,最后在一个哥们写的文章的最后找到了答案,大概的意思就是说在容器处理request的过程中,request是会被复用的,这会导致如果主线程比子线程执行的早完成,那么此时request被复用下一个请求,子线程手里拿到的request其实就指向了内存中下一个请求的request了。这个方法不行。
②,其次我又查询文档,发现另外一种解决方案。
ServletRequestAttributes request = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(request, true);
但是这种方法同样有主线程先于子线程执行完的时候,request丢失的请求,也是不行。
4,于是我打算自己弄,尝试用ThreadLocal来做。
①,首先定义一个ThreadLocal副本
public class ThreadLocalMap {
public final static ThreadLocal<String> threadLocal = new ThreadLocal<>();
}
②,再定义一个线程类,此类是为了增强run方法,同时也是维护ThreadLocal副本变量内容。
public class RequestThread extends Thread{
RequestThread(Runnable target){
super(target);
}
private String authorization;
@Override
public void run() {
ThreadLocalMap.threadLocal.set(authorization);
super.run();
ThreadLocalMap.threadLocal.remove();
}
public void setAuthorization(String authorization) {
this.authorization = authorization;
}
}
③,再新增一个线程池子类,这里是为了能增强execute方法和submit方法,从主线程中获取request变量再透传下去,也是为了使用上面定义的RequestThread类包装一下原本要提交的task。
public class RequestThreadPoolExecutor extends ThreadPoolExecutor{
public RequestThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public <T> Future<T> submit(Runnable task, T result) {
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
String authorization = request.getHeader("Authorization");
RequestThread requestThread = new RequestThread(task);
requestThread.setAuthorization(authorization);
return super.submit(requestThread, result);
}
@Override
public void execute(Runnable task){
HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest();
String authorization = request.getHeader("Authorization");
RequestThread requestThread = new RequestThread(task);
requestThread.setAuthorization(authorization);
super.execute(requestThread);
}
}
④,线程配置类,使用自定义线程池
@Configuration
public class ThreadPoolConfig {
@Bean(value = "requestThreadPoolExecutor")
public RequestThreadPoolExecutor threadPoolExecutor(){
return new RequestThreadPoolExecutor(2, 2, 10L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(30));
}
}
⑤,测试一下,这里我提交了一个异步线程任务,在里面打印了从ThreadLocal获取的request存储内容,然后定时sleep随机20秒,是为了两个线程会随机哪个线程先执行完然后获取队列中的下一个request请求提交的任务。
@RestController
@RequestMapping(value = "/api/request")
@RequiredArgsConstructor
public class TestController {
private final ThreadPoolConfig.RequestThreadPoolExecutor threadPoolExecutor;
@GetMapping(value = "/thread")
public String testThread(){
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
String authorization = ThreadLocalMap.threadLocal.get();
String name = Thread.currentThread().getName();
System.out.println("时间:"+System.currentTimeMillis() + "当前线程:"+name + "获取变量:"+authorization);
try {
Random r = new Random();
int i = r.nextInt(20000);
Thread.sleep(i);
}catch (Exception e) {
e.printStackTrace();
}
}
});
return "success";
}
}
⑥,测试结果可以看到,我随机发送了rquest header 不同的信息,两个异步线程随机获取执行,结果无误。