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

 更新时间:2024年08月15日 08:47:49   作者:毅航  
这篇文章主要介绍了我们就对多线程环境下使用SpringMVC中RequestContextHolder无法获取请求的问题进行了深入的分析,并针对相关问题给出了相应的解决方案,需要的朋友可以参考下

前言

在日常的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 对象。而ServletRequestAttributesSpring MVC中主要用于访问和管理与当前HTTP请求相关的属性, 并且提供了对HttpServletRequestHttpServletResponse对象的访问的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对象,从而出现了空指针的异常。

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

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

解决方案

在这里我们先对网上一种错误的方案进行纠正。对于多线程环境下无法获取请求头的这一问题,网上其实很早就有人给出了解决方案,其大致思路是调用RequestContextHoldersetRequestAttributesinheritable属性置为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而言,当我们指定其requestAttributestrue时,其会将相关的请求信息放入到InheritableThreadLocal中。而InheritableThreadLocalThreadLocal 的子类,其可以实现父线程和子线程之间数据的共享。因此当使用 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-LANGen,当请求get-request-header-in-thread这一路径后,执行结果如下:

可以看到,两行日志打印时间间隔相差5秒中,而这5秒恰好正是我们代码中Sleep的时间。进一步,子线程打印出的内容zh-en。即在子线程中其在获取请求头时,本质是获取到了我们在getLanguages定义的默认内容,而非我们请求头中X-CLIENT-LANG对应的en。换言之,网上流传的将RequestContextHolder而言,当我们指定其requestAttributestrue能有效解决多线程下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设置到子线程中,从而是确保实现子线程也能获取到相关的请求对象。

总结

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

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

以上就是SpringMVC在多线程下请求头获取失败问题的解决方案的详细内容,更多关于SpringMVC请求头获取失败的资料请关注脚本之家其它相关文章!

相关文章

  • Java实现添加、验证PDF数字签名的方法示例

    Java实现添加、验证PDF数字签名的方法示例

    在设置文档内容保护的方法中,除了对文档加密、添加水印外,应用数字签名也是一种有效防伪手段。本文就使用Java实现添加、验证PDF数字签名,感兴趣的可以了解一下
    2021-07-07
  • 通过实例解析spring环绕通知原理及用法

    通过实例解析spring环绕通知原理及用法

    这篇文章主要介绍了通过实例解析spring环绕通知原理及用法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-10-10
  • SpringBoot基于knife4j生成接口文档方式

    SpringBoot基于knife4j生成接口文档方式

    Knife4j介绍一个基于Swagger的JavaAPI文档生成工具,提供美观界面和高级功能,本文详细介绍了在SpringBoot项目中集成Knife4j的方法,包括导入依赖、创建Swagger配置和WebMvcConfiguration配置类等
    2026-05-05
  • Java打印高质量日志的10条方法详解

    Java打印高质量日志的10条方法详解

    你以为打日志是小事,也许正是这种轻视,让你在凌晨三点被生产事故电话吵醒,一个优秀的工程师和普通码农的区别,往往体现在那些看似微不足道的细节上,下面我们就来看看如何打印高质量日志吧
    2025-06-06
  • Nacos多个实例的服务调用失败问题及解决

    Nacos多个实例的服务调用失败问题及解决

    在微服务开发中,Nacos上出现多个同一服务实例导致OpenFeign调用失败,因为OpenFeign使用SpringCloudLoadbalancer,而SpringCloudLoadbalancer的底层默认负载均衡策略不支持Nacos,通过将负载均衡策略设置为Nacos,可以解决调用失败的问题
    2026-02-02
  • SpringBoot整合MinIO实现文件存储系统的代码示例

    SpringBoot整合MinIO实现文件存储系统的代码示例

    在现代的应用程序中,文件存储和管理是一个常见的需求,MinIO是一个开源的对象存储系统,与Spring Boot框架结合使用,可以快速构建高性能的文件存储系统,本文将介绍如何使用Spring Boot和MinIO来实现文件存储系统
    2023-06-06
  • springboot集成mybatisplus的详细步骤

    springboot集成mybatisplus的详细步骤

    MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生,这篇文章主要介绍了springboot四步集成mybatisplus,需要的朋友可以参考下
    2022-10-10
  • 详解如何在Java中重写equals()和hashCode()方法

    详解如何在Java中重写equals()和hashCode()方法

    在 Java 中,equals() 和 hashCode() 方法是 Object 类中定义的重要方法,它们用于比较对象的相等性以及计算对象的哈希值,本文将详细介绍如何在 Java 中重写 equals() 和 hashCode() 方法,并讨论其最佳实践,需要的朋友可以参考下
    2024-08-08
  • mybatis中resulthandler的用法

    mybatis中resulthandler的用法

    这篇文章主要介绍了mybatis中resulthandler的用法,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • MyBatis-Plus 中 typeHandler 的使用实例详解

    MyBatis-Plus 中 typeHandler 的使用实例详解

    本文介绍了在MyBatis-Plus中如何使用typeHandler处理json格式字段和自定义typeHandler,通过使用JacksonTypeHandler,可以简单实现将实体类字段转换为json格式存储,感兴趣的朋友跟随小编一起看看吧
    2024-10-10

最新评论