Spring异步调用传递Request对象问题分析

一、问题描述

近期在实验室做了个动态插桩工具,在对甲方项目测试过程中,发现对含有线程池异步调用的方法进行插桩时,子线程会报空指针异常。

二、问题原因

动态插桩工具向待测软件注入的代码中包含了如下语句,利用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);设置子线程共享。

  1. RequestContextHolder类里面包含两个ThreadLocal全局属性:一个是控制主线程的ThreadLocal,一个继承自InheritableThreadLocal用来子线程共享父线程request对象。

  2. 在setRequestAttributes中,如果第二个参数值为true,则会把request属性放入InheritableThreadLocal中,并在ThreadLocal中删除主线程信息;如果为false会把request属性放入ThreadLocal中,并在InheritableThreadLocal中删除主线程信息。

  3. 当子线程调用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;
}

局限性

  1. 这种共享方式只适合于主线程等待子线程完成任务后再结束的情况,否则主线程先于子线程结束的话,主线程的 request 会被销毁,子线程还是共享不了主线程的request属性。
  2. 不适用于线程池间上下文传递

关于上述局限性的第二点,关键核心在于inheritableThreadLocal对象上。

InheritableThreadLocal是在创建线程时调用init()方法赋值的,而线程池中的线程是复用的,所以这里会存在问题。

阿里巴巴开源了一个Transmittable-Thread-Local组件可以解决这个问题

关于ThreadLocal与inheritableThreadLocal源码级的详解,可参考本人的另一篇文章:面试:Java多线程基础知识

参考文章

多线程异步操作子线程获取不到主线程request信息问题解决及源码解析
如何在 Spring 异步调用中传递上下文


版权声明:本文为qq_36198826原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。