RequestContextHolder跨线程获取不到requests请求对象的解决方法

发布时间 2023-07-11 10:41:34作者: 卧龙戏公瑾

一、前言

最近在做一个项目,有个比较耗时的操作是启用线程进行异步操作,当时在启用的线程时,突然发现子线程无法获取父线程中的HttpServletRequest请求对象,因为是第一次遇到这种问题,所以记录一下解决方案。

二、问题模拟

在这里,我们简单模拟一下出现的问题。我们首先编写一个简单的hello请求,代码如下:

 /**
     * 主线程获取
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        String name = "";
        HttpServletRequest request = RequestUtils.getRequest();
        if (null == request) {
            log.info("未获取到request对象!");
        } else {
            name = request.getParameter("name");
            log.info("获取到的内容为{}", name);
        }

        return "hello";
    }

这是一个正常的请求,我们启动项目,访问接口地址。
图一
图二

从上图中,我们不难发现,我们成功的拿到了HttpServletRequest中的参数。

接着,我们稍微修改一下我们的代码,另起一个线程,在子线程中获取HttpServletRequest中的name属性,代码如下:

 /**
     * 主线程获取
     * @return
     */
    @GetMapping("/hello")
    public String hello() {

        new Thread(() -> {
            HttpServletRequest request = RequestUtils.getRequest();
            if (null == request) {
                log.info("未获取到request对象!");
            } else {
                String name = request.getParameter("name");
                log.info("获取到的内容为{}", name);
            }
        }).start();


        return "hello";
    }

我们再次启动项目并访问接口地址:
图一
图二

我们发现,这时候的request对象已经变为空,我们根本没办法获取请求中的name属性。

结论:如果采用多线程,我们就获取不到父线程中的HttpServletRequest对象了。

三、解决方法

解决上面的问题其实很简单,只需要在开启子线程时,调用一下 RequestContextHolder.setRequestAttributes(requestAttributes, true);方法,将第二个参数设为true就可以了。

我们修改上面的代码如下:

/**
     * 主线程获取
     * @return
     */
    @GetMapping("/hello")
    public String hello() {
        /**
         * 解决子线程无法获取HttpServletRequest请求对象中数据的问题
         */
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        RequestContextHolder.setRequestAttributes(requestAttributes, true);
        new Thread(() -> {
            HttpServletRequest request = RequestUtils.getRequest();
            if (null == request) {
                log.info("未获取到request对象!");
            } else {
                String name = request.getParameter("name");
                log.info("获取到的内容为{}", name);
            }
        }).start();


        return "hello";
    }

启动项目,访问接口地址,结果如下:
图一
图二
可以发现,我们可以在子线程中获取HttpServletRequest对象了。

四、原理

点开RequestContextHolder.setRequestAttributes(requestAttributes, true)方法,查看源码:

	/**
	 * Bind the given RequestAttributes to the current thread.
	 * @param attributes the RequestAttributes to expose,
	 * or {@code null} to reset the thread-bound context
	 * @param inheritable whether to expose the RequestAttributes as inheritable
	 * for child threads (using an {@link InheritableThreadLocal})
	 */
	public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
		if (attributes == null) {
			resetRequestAttributes();
		}
		else {
			if (inheritable) {
				inheritableRequestAttributesHolder.set(attributes);
				requestAttributesHolder.remove();
			}
			else {
				requestAttributesHolder.set(attributes);
				inheritableRequestAttributesHolder.remove();
			}
		}
	}

这个方法很简单,主要只要继续查看requestAttributesHolderinheritableRequestAttributesHolder这两个类的继承关系就可以了。

requestAttributesHolder

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
			new NamedThreadLocal<>("Request attributes");


public class NamedThreadLocal<T> extends ThreadLocal<T> {}

我们发现,requestAttributesHolder对象类型为NamedThreadLocal,NamedThreadLocal父类是ThreadLocal。

inheritableRequestAttributesHolder

private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
			new NamedInheritableThreadLocal<>("Request context");

public class NamedInheritableThreadLocal<T> extends InheritableThreadLocal<T> {}

我们发现inheritableRequestAttributesHolder的类型为NamedInheritableThreadLocal,NamedInheritableThreadLocal是InheritableThreadLocal的子类。

看到这里,就很清晰了。调用RequestContextHolder.setRequestAttributes(requestAttributes, true)这个方法,将原本放在ThreadLocal对象中的属性放到了类型为InheritableThreadLocal的对象中了,所以我们启动的子线程可以获取到父线程中的属性。

五、总结

当子线程中无法获取父线程中的HttpServletRequest的方法时,我们可以通过调用RequestContextHolder.setRequestAttributes(requestAttributes, true)方法,使得子线程也可以获取父线程中HttpServletRequest对象。