一、问题描述
近期在实验室做了个动态插桩工具,在对甲方项目测试过程中,发现对含有线程池异步调用的方法进行插桩时,子线程会报空指针异常。
二、问题原因
动态插桩工具向待测软件注入的代码中包含了如下语句,利用spring提供的RequestContextHolder获取Request对象。
ServletRequestAttributes sra$2 = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request$im = (sra$2).getRequest();
该HttpServletRequest对象在子线程中获取不到,导致报空指针异常。
三、解决方法
1、通过setRequestAttributes(sra$, true);有局限性
spring提供的这个方法默认子线程不能共享父线程的HttpServletRequest对象。网上有人采用如下方法用来子线程共享父线程的Request变量。
ServletRequestAttributes sra$ = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
RequestContextHolder.setRequestAttributes(sra$, true);
上述做法不能解决线程池中异步调用子线程的问题,只适用于主线程等待子线程完成任务后再结束的情况,否则主线程先于子线程结束的话,主线程的 request 会被销毁,子线程还是共享不了主线程的request属性。
解决方法:对于直接在spring里编写源代码,可参考以下文章,本人没经过测试。
2、将需要传递的值注入到自定义的对象中,然后将这个对象作为参数传递给子线程
四、setRequestAttributes(sra$, true)局限性原理分析
通过RequestContextHolder.setRequestAttributes(servletRequestAttributes, true);
设置子线程共享。
RequestContextHolder类里面包含两个ThreadLocal全局属性:一个是控制主线程的ThreadLocal,一个继承自InheritableThreadLocal用来子线程共享父线程request对象。
在setRequestAttributes中,如果第二个参数值为true,则会把request属性放入InheritableThreadLocal中,并在ThreadLocal中删除主线程信息;如果为false会把request属性放入ThreadLocal中,并在InheritableThreadLocal中删除主线程信息。
当子线程调用
getRequestAttributes()
获取主线程的request的属性时,如果子线程中ThreadLocal.get()获取属性为空,就会从inheritableThreadLocal 中去获取属性.
//类上共享变量
private static final boolean jsfPresent =
ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
//控制主线程的 ThreadLocal
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
//控制子线程的 ThreadLocal
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
//核心源码
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {//传true
// 就是把request的属性放入到inheritableRequestAttributesHolder 中,供子类继承
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();//子线程中删除主线程的信息
}
else {//传false
requestAttributesHolder.set(attributes);//设置主线程使用
inheritableRequestAttributesHolder.remove();//不共享设置
}
}
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {//负责主线程的信息为空,就去负责子线程的作用域中取
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
局限性
- 这种共享方式只适合于主线程等待子线程完成任务后再结束的情况,否则主线程先于子线程结束的话,主线程的 request 会被销毁,子线程还是共享不了主线程的request属性。
- 不适用于线程池间上下文传递
关于上述局限性的第二点,关键核心在于inheritableThreadLocal对象上。
InheritableThreadLocal是在创建线程时调用init()方法赋值的,而线程池中的线程是复用的,所以这里会存在问题。
阿里巴巴开源了一个Transmittable-Thread-Local
组件可以解决这个问题
关于ThreadLocal与inheritableThreadLocal源码级的详解,可参考本人的另一篇文章:面试:Java多线程基础知识