Spring Boot 异步线程静态获取request对象为空 RequestContextHolder 为空 Java 异步线程获取request为空
一、问题描述
在Spring Boot的web项目中,采用静态获取request对象时,发现无法获取到request对象,而获取的 RequestContextHolder 对象为空,抛出 NPE 异常 ...
public static HttpServletRequest getRequest() {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = servletRequestAttributes.getRequest();
return request ;
}
经过排查代码,发现是在异步线程中,静态获取request对象,导致获取不到,从而抛出NPE异常...
二、模拟实现
1、演示:异步线程中无法获取到request对象,抛出NPE异常
@RequestMapping("/req")
public String req(){
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(()->{
log.info(Thread.currentThread().getName() + " start ===>");
String token = null;
try {
Thread.sleep(1000);
token = RequestUtil.getRequest().getHeader("token");
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e){
/**
* 注意: 需要增加 catch Exception 异常 ;
* 否则: RequestUtil.getRequest() 的 NPE 异常无法抛出!
*/
e.printStackTrace();
}
log.info(Thread.currentThread().getName() + " end token ===>{}", token);
});
return "ok";
}
2、输出结果如下:
INFO] com.runcode.springboottourist.RequController:40 : pool-8-thread-1 start ===>
java.lang.NullPointerException
at com.runcode.springboottourist.util.RequestUtil.getRequest(RequestUtil.java:29)
at com.runcode.springboottourist.RequController.lambda$req$0(RequController.java:44)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
[INFO] com.runcode.springboottourist.RequController:54 : pool-8-thread-1 end token ===>null
三、解决
1、只需要设置 request 对象可以在子线程中共享即可,在 主线程代码部分设置即可。
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);
2、完整代码参考如下:
@RequestMapping("/req/fix")
public String reqFix(){
// 设置request 对象在,子线程(异步线程)中可以共享
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);
HttpServletRequest request = RequestUtil.getRequest();
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(()->{
log.info(Thread.currentThread().getName() + " start ===>");
String token = null;
try {
Thread.sleep(1000);
token = request.getHeader("token");
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
log.info(Thread.currentThread().getName() + " end token ===> {}", token);
});
return "token=" + RequestUtil.getRequest().getHeader("token");
}
四、总结
1、在写异步线程代码时,一定要注意异常情况的捕获和处理;若未正确的捕获或处理异常,会导致程序没有达到预期的执行结果,且没有任何异常输出,造成出现问题,难以排查的情况。
1.1、未正确的处理异常情况:
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(()->{
try {
Thread.sleep(1000);
/**
* 未正确的捕获异常:
* InterruptedException 无法捕获 xx 异常
*/
int a = 3/ 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" ===> 异步线程执行结束!");
});
System.out.println("main 执行结束 ");
}
1.2、输出结果:
main 执行结束
1.3、未正确的捕获异常:
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(()->{
try {
Thread.sleep(1000);
/**
* 未正确的捕获异常:
* InterruptedException 无法捕获 xx 异常
*/
int a = 3/ 0;
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" ===> 异步线程执行结束!");
});
System.out.println("main 执行结束 ");
}
2、正确的处理异常情况: 最后一级增加 Exception 捕获
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(()->{
try {
Thread.sleep(1000);
/**
* 未正确的捕获异常:
* InterruptedException 无法捕获 ArithmeticException 异常
*/
int a = 3/ 0;
} catch (InterruptedException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" ===> 异步线程执行结束!");
});
System.out.println("main 执行结束 ");
}
2.1、输出结果:
main 执行结束
java.lang.ArithmeticException: / by zero
at com.runcode.springboottourist.RequController.lambda$main$3(RequController.java:154)
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
at java.lang.Thread.run(Thread.java:745)
pool-1-thread-1 ===> 异步线程执行结束!
3、本文仅仅是解决:异步线程中无法获取request对象的问题;对于主线程结束后,异步线程获取到的 request 对象,会存在request 对象获取到的方法参数都为空的情况,例如:
// 主线程可以正常获取到参数, 异步线程中获取到的是null
RequestUtil.getRequest().getHeader("token");
建议解决办法: 主线程中获取到,以参数形式传递到子线程、存到redis中、重写request对象等方法进行尝试解决。
4、RequestContextHolder 方法的实现,点进去源码进行查看,里面有2个 ThreadLocal 对象,是可以解决 父子线程,变量共享的问题,请自行研究。