解决SpringMVC在多线程下请求头获取失败的问题

深入剖析SpringMVC多线程下无法获取请求的原因中我们对SpringMVC在多线程情况下无法获取请求头中字段的问题进行了分析。接下来,我们便针对这一问题提出相应的应对之策。

前言

在日常的SpringMVC开发中,我们通常会在请求头中自定义一些参数信息,之后借助SpringMVC提供的RequestContextHolder来完成当前请求的获取,此时代码逻辑大致如下:

 

java

代码解读

复制代码

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请求相关的属性, 并且提供了对HttpServletRequestHttpServletResponse对象的访问的API

进一步,当获取到ServletRequestAttributes对象后,我们就可以通过其提供的getRequest来获取到当前请求的Reqeust对象。而当获取到当请求的Reqeust对象后,我们即可读取请求头,从而获取到请求头中自定义的key-value键值对。

请求头丢失的问题

如果是在单线程情况下,上述逻辑不存在任何问题。但如果是多线程环境下,你会发现程序会莫名其妙出现空指针异常。此时出现的问题具体如下:

Controller测试接口

 

java

代码解读

复制代码

@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()具体逻辑

整理了一份好像面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处即可】免费获取

java

代码解读

复制代码

@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对象,从而出现了空指针的异常。

而这恰恰也是我们开发中常见的在多线程环境下请求头丢失的问题,造成这一问题的原因我们曾在深入剖析SpringMVC多线程下无法获取请求的原因有进行过深入分析。简单来看,对于SpringMVC而言,每个请求request信息是存储在ThreadLocal中,而对于ThreadLocal而言,其key为当前线程,因此每个线程一个存储份Request对象,因此Request对象只与当前线程关联。如果,我们尝试在当前线程中,再启动一个子线程去获取Reqeust其必然是无法获取到主线程的Request对象。

进一步,针对多线程环境下无法获取请求的这一问题,笔者在此提供两个解决思路。希望对你能有所启发。

解决方案

在这里我们先对网上一种错误的方案进行纠正。对于多线程环境下无法获取请求头的这一问题,网上其实很早就有人给出了解决方案,其大致思路是调用RequestContextHoldersetRequestAttributesinheritable属性置为true,从而实现父子线程对于Request对象的共享。之所以这么做的原因在于SpringMVC中有如下的代码:

 

java

代码解读

复制代码

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而言,当我们指定其requestAttributestrue时,其会将相关的请求信息放入到InheritableThreadLocal中。而InheritableThreadLocal 是 ThreadLocal 的子类,其可以实现父线程和子线程之间数据的共享。因此当使用 InheritableThreadLocal 保存数据时,子线程在创建时会继承父线程中的 ThreadLocal 变量值。通过这样的方式从而实现多线程环境下请求的获取。

但这样做的前提在于其必须确保子线程一定在父线程后执行完毕,而如果子线程执行慢,父线程执行较快,已经会存在子线程中数据获取的问题!这么说可能比较晦涩,接下来我们不妨通过一个简单的例子来分析这一方法存在的问题:

 

java

代码解读

复制代码

@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方法中重新一个子线程去尝试获取请求中的语言信息。而我们的请求如下:

image.png

在请求头中,我们设定的本次请求的语言头为X-CLIENT-LANGen,当请求get-request-header-in-thread这一路径后,执行结果如下:

image.png

可以看到,两行日志打印时间间隔相差5秒中,而这5秒恰好正是我们代码中Sleep的时间。进一步,子线程打印出的内容zh-en。即在子线程中其在获取请求头时,本质是获取到了我们在getLanguages定义的默认内容,而非我们请求头中X-CLIENT-LANG对应的en。换言之,网上流传的将RequestContextHolder而言,当我们指定其requestAttributestrue能有效解决多线程下SpringMVC中获取请求的方案完全是有问题的。那如何能解决这一问题呢?其实也很简单,如果能确保只开启有限线程的话,完全可以借助CountDownLatch来实现多线程间的协调工作。改造后的代码如下:

 

java

代码解读

复制代码

@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的操作来实现请求的共享。其具体逻辑如下:

 

java

代码解读

复制代码

@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设置到子线程中,从而是确保实现子线程也能获取到相关的请求对象。

总结

至此,我们就对多线程环境下使用SpringMVCRequestContextHolder无法获取请求的问题进行了深入的分析,并针对相关问题给出了相应的解决方案。具体来看,造成多线程环境下请求无法获取的原因在于在默认情况下SpringMVC内部对于请求头的存放于在ThnreadLocal。而如果手动对RequestContextHolder中的inheritable设定为True,其会将请求头存放于InheritableThreadLocal,从而实现父子线程请求头的共享。

但当请求头存放于InheritableThreadLocal时,如果父线程先销毁,则子线程依旧存在无法获取请求头的问题。 针对这一问题,我们给出了线程同步的解决方案。同时,还给出了更加通用的方案以彻底解决多线程环境下请求头丢失的问题。

  • 11
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值