解决SpringMVC在多线程下请求头获取失败的问题深入剖析SpringMVC多线程下无法获取请求的原因。同时,针对这一
思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。 作者:毅航😜
前言
在日常的SpringMVC
开发中,我们通常会在请求头中自定义一些参数信息,之后借助SpringMVC
提供的RequestContextHolder
来完成当前请求的获取,此时代码逻辑大致如下:
public static HttpServletRequest getRequest() {
HttpServletRequest httpServletRequest = null;
try {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes instanceof ServletRequestAttributes) {
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
httpServletRequest = servletRequestAttributes.getRequest();
}
} catch (Exception e) {
// 记录异常,但不向外抛出,以避免可能的业务逻辑中断
log.error("获取HttpServletRequest时发生异常:", e);
}
// 返回获取到的请求对象,如果失败则返回null
return httpServletRequest;
}
上述代码中,我们首先通过RequestContextHolder
提供的getRequestAttributes
方法获取到一个 ServletRequestAttributes
对象。而ServletRequestAttributes
在Spring MVC
中主要用于访问和管理与当前HTTP
请求相关的属性, 并且提供了对HttpServletRequest
和HttpServletResponse
对象的访问的API
。
进一步,当获取到ServletRequestAttributes
对象后,我们就可以通过其提供的getRequest
来获取到当前请求的Reqeust
对象。而当获取到当请求的Reqeust
对象后,我们即可读取请求头,从而获取到请求头中自定义的key-value
键值对。
请求头丢失的问题
如果是在单线程情况下,上述逻辑不存在任何问题。但如果是多线程环境下,你会发现程序会莫名其妙出现空指针异常。此时出现的问题具体如下:
Controller
测试接口
@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
@GetMapping("/missing-request-header")
public String getMissingRequestHeader() {
// 主线程获取请求头信息
String mainThreadLanguages = ServletUtils.getLanguagesExistProblem();
log.info("主线程获取请求头信息:{}", mainThreadLanguages);
new Thread(() -> {
// 子线程获取请求头信息
String subThreadLanguages = ServletUtils.getLanguagesExistProblem();
log.info("子线程获取请求头信息:{}", subThreadLanguages);
}).start();
return "success";
}
}
ServletUtils.getLanguagesExistProblem()
具体逻辑
@Slf4j
public class ServletUtils {
private final static String X_CLIENT_LANG = "X-CLIENT-LANG";
public static String getLanguagesExistProblem() {
HttpServletRequest request = getRequest();
Assert.notNull(request);
String lang = request.getHeader(X_CLIENT_LANG);
if (StrUtil.isNotBlank(lang)) {
return lang;
}
return "zh-cn";
}
}
在上述代码中,我们在TestController
中启用了一个新的线程,尝试去通过getLanguagesExistProblem
读取请求头中我们自定义的"X-CLIENT-LANG
头信息。然而,当运行代码后你会发现出现代码无法通过Assert.notNull(request);
这个断言信息。即当子线程尝试去读取请求中的"X-CLIENT-LANG
信息时,其在子线程中无法获取到当前请求中的Request
对象,从而出现了空指针的异常。
进一步,针对多线程环境下无法获取请求的这一问题,笔者在此提供两个解决思路。希望对你能有所启发。
解决方案
在这里我们先对网上一种错误的方案进行纠正。对于多线程环境下无法获取请求头的这一问题,网上其实很早就有人给出了解决方案,其大致思路是调用RequestContextHolder
的setRequestAttributes
将inheritable
属性置为true
,从而实现父子线程对于Request
对象的共享。之所以这么做的原因在于SpringMVC
中有如下的代码:
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();
}
}
}
在SpringMVC
内对,对于RequestContextHolder
而言,当我们指定其requestAttributes
为true
时,其会将相关的请求信息放入到InheritableThreadLocal
中。而InheritableThreadLocal
是 ThreadLocal
的子类,其可以实现父线程和子线程之间数据的共享。因此当使用 InheritableThreadLocal
保存数据时,子线程在创建时会继承父线程中的 ThreadLocal
变量值。通过这样的方式从而实现多线程环境下请求的获取。
但这样做的前提在于其必须确保子线程一定在父线程后执行完毕,而如果子线程执行慢,父线程执行较快,已经会存在子线程中数据获取的问题!这么说可能比较晦涩,接下来我们不妨通过一个简单的例子来分析这一方法存在的问题:
@GetMapping("/get-request-header-in-thread")
public String getRequestHeaderInThread() {
// 主线程获取请求头信息
String mainThreadLanguages = ServletUtils.getLanguages();
log.info("主线程获取请求头信息:{}", mainThreadLanguages);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 子线程获取请求头信息
String subThreadLanguages = ServletUtils.getLanguages();
log.info("子线程获取请求头信息:{}", subThreadLanguages);
}).start();
return "success";
}
(注:此处的ServletUtils.getLanguages()
逻辑可参考之前代码)
在上述代码中,我们在getRequestHeaderInThread
方法中重新一个子线程去尝试获取请求中的语言信息。而我们的请求如下:
在请求头中,我们设定的本次请求的语言头为X-CLIENT-LANG
为en
,当请求get-request-header-in-thread
这一路径后,执行结果如下:
可以看到,两行日志打印时间间隔相差5秒
中,而这5秒
恰好正是我们代码中Sleep
的时间。进一步,子线程打印出的内容zh-en
。即在子线程中其在获取请求头时,本质是获取到了我们在getLanguages
定义的默认内容,而非我们请求头中X-CLIENT-LANG
对应的en
。换言之,网上流传的将RequestContextHolder
而言,当我们指定其requestAttributes
为true
能有效解决多线程下SpringMVC
中获取请求的方案完全是有问题的。那如何能解决这一问题呢?其实也很简单,如果能确保只开启有限线程的话,完全可以借助CountDownLatch
来实现多线程间的协调工作。改造后的代码如下:
@GetMapping("/get-request-header-in-thread")
public String getRequestHeaderInThread() {
// 主线程获取请求头信息
String mainThreadLanguages = ServletUtils.getLanguages();
CountDownLatch latch = new CountDownLatch(1);
log.info("主线程获取请求头信息:{}", mainThreadLanguages);
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 子线程获取请求头信息
String subThreadLanguages = ServletUtils.getLanguages();
log.info("子线程获取请求头信息:{}", subThreadLanguages);
latch.countDown();
}).start();
// 等待计数器变为零
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
log.info("确保父子线程全部执行完毕");
return "success";
}
但在开发中,如果遇到子线程比较耗时的操作,上述代码的性能又成为了效率的瓶颈。这与我们使用多线程开发的初衷相悖。事实上上,除了上述的方案外,我们还可以采用缓存当前Request
的操作来实现请求的共享。其具体逻辑如下:
@GetMapping("/get-request-header-in-async-thread/{isJoin}")
public String getRequestHeaderInThread() {
// 主线程获取请求头信息
String mainThreadLanguages = ServletUtils.getLanguages();
log.info("主线程获取请求头信息:{}", mainThreadLanguages);
// 获取当前servletRequestAttributes对象
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
new Thread(() -> {
// 将servletRequestAttributes设定到子线程中
RequestContextHolder.setRequestAttributes(servletRequestAttributes);
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 子线程获取请求头信息
String subThreadLanguages = ServletUtils.getLanguages();
log.info("子线程获取请求头信息:{}", subThreadLanguages);
}).start();
return "success";
}
在上述代码中,我们手动获取到当前线程servletRequestAttributes
对象,然后将子线程代码执行前, 手动给主线程中的ServletRequestAttributes
设置到子线程中,从而是确保实现子线程也能获取到相关的请求对象。
总结
至此,我们就对多线程环境下使用SpringMVC
中RequestContextHolder
无法获取请求的问题进行了深入的分析,并针对相关问题给出了相应的解决方案。具体来看,造成多线程环境下请求无法获取的原因在于在默认情况下SpringMVC
内部对于请求头的存放于在ThnreadLocal
。而如果手动对RequestContextHolder
中的inheritable
设定为True
,其会将请求头存放于InheritableThreadLocal
,从而实现父子线程请求头的共享。
但当请求头存放于InheritableThreadLocal
时,如果父线程先销毁,则子线程依旧存在无法获取请求头的问题。 针对这一问题,我们给出了线程同步的解决方案。同时,还给出了更加通用的方案以彻底解决多线程环境下请求头丢失的问题。
转载自:https://juejin.cn/post/7402873035915067442