SpringMVC多线程下无法获取请求的原因和解决方法

 更新时间:2024年04月22日 08:54:22   作者:毅航  
从问题出发,逐步讲透在SpringMVC中使用RequestContextHolder对象在多线程情况下无法获取请求的真实原因,本文将和大家一起深入剖析SpringMVC多线程下无法获取请求的原因和解决,需要的朋友可以参考下

前言

众所周知,在SpringMVC中如果我们期待获取当前请求的HttpServletRequest对象,通常有如下几种方式:

  • 通过方法参数注入:在Controller的方法中,可以直接声明HttpServletRequest类型的参数,Spring MVC会自动将当前请求的HttpServletRequest对象注入进来。例如:
@Controller
public class MyController {

    @RequestMapping("/example")
    public String handleRequest(HttpServletRequest request) {
        // 使用request对象
        return "example";
    }
}
  • 通过RequestContextHolderSpring MVC提供了一个RequestContextHolder类,例如:
HttpServletRequest request = ((ServletRequestAttributes) 
RequestContextHolder.getRequestAttributes()).getRequest();

而本次我们重点分析当使用RequestContextHolder在多线程环境下获取请求所可能导致的一些问题。

(注:在获取当前请求的HttpServletRequest我们会利用RequestContextHolder先获取到ServletRequestAttributes后,再通过ServletRequestAttributes来获取对应的httpServletRequest对象)

问题复现

为了直观的理解RequestContextHolder在多线程使用下所导致的问题,我们先来通过一个业务中的真实场景来进行分析。

在国际化功能开发中,我们通常会将用户当前的语言信息存放在Request请求中,这样后端通过获取请求头的中的语言信息就能成功获取到用户所支持的语言。

进一步,对于一些涉及到国际化的导入,导出的耗时操作来说,我们通常会将其放在异步线程中进行执行,以提升程序性能。代码逻辑大致如下:

@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()方法的调用。

该方法是笔者所写的一个工具类,其主要作用就是获取当前请求头中的Lang属性,方法内部具体逻辑如下所示:

    /**
     * 获取客户端请求头中的语言信息。
     * 其会从当前的HTTP请求中提取客户端所设置的语言信息。
     * 主要通过读取请求头中的"X_CLIENT_LANG"字段来获取客户端语言偏好。
     *
     * @return String 客户端请求头中指定的语言信息。
     * 如果不存在该字段则返回默认的zh-cn。
     */
    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";
    }

可以看到,在getLanguagesExistProblem方法内部又会通过getRequest获取到当前请求的HttpServletRequest信息,而getRequest内部逻辑如下所示:

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;
}

整体来看上述代码调用逻辑如下:

至此,相信你对于示例代码的逻辑其实已经清楚了。其无非就是会首先会在主线程中获取当前请求头中的语言信息,接着,又会新建一个子线程尝试去获取到请求头中的语言信息。看起来似乎代码似乎没什么问题,尝试执行代码,你会发现有如下提示:

追踪溯源

通过错误提示不难发现是子线程在调用getLanguagesExistProblem()方法时所提示的错误。具体来看,是因为其内部Assert.notNull(request);断言所提示的错误,而导致问题发生的原因是因为传入的request对象为null,进而导致不满足断言条件notNull从而提示异常信息。

正如我们前面所说,我们的request对象是通过RequestContextHolder来获取的。具体我们的代码来看,其本质是通过ServletRequestAttributesgetRequest来完成这一操作。那为什么会在多线程情况下有这样的问题呢?初看这一问题你可能会有点摸不到头脑,不知该如何下手。没有思路也别慌,接下来,不妨听一听笔者是如何对这一问题进行分析的。

首先,既然RequestContextHolder可以实现获取当前请求的功能,其一定会把请求进行一层缓存,以确保我们无论在程序的任何位置都能获取到请求,顺着这一思路,如果要你来实现这一需求,你会如何设计呢?

我想你大概率会将此处的逻辑设计在程序公共的入口位置,那SpringMVC中请求第一次进入时公共的会首先在哪处理呢?显示是Servlet中的service方法。具体到DispatcherServlet来看,其内部逻辑如下所示:

@Override
protected void service(HttpServletRequest request, 
                        HttpServletResponse response)
        {

      // ... 省略其他无关代码
 
     // 处理请求核心代码,点击该方法进入
     processRequest(request, response); 
 
}

(Ps: 这里需要读者有一点Servlet相关知识,简单来看,所有请求进入Servlet后都会先通过Service方法的处理~~~)

不难发现,service方法其内部的核心逻辑会委托processRequest进行处理,而processRequest其内部逻辑如下:

protected final void processRequest(HttpServletRequest request, 
    // ... 省略其他无关逻辑
    // 初始化ContextHolders, 便于访问上下文信息
    initContextHolders(request, localeContext, requestAttributes);
  
    doService(request, response);
  }

通过initContextHolders方法的名称我们不难猜出,其大概的作用微在于初始化一个ContextHolder相关属性。事实上,在该方法内其会将 requestAttributes与当前线程进行绑定。具体逻辑如下:

private void initContextHolders(HttpServletRequest request,
                                @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
    // 将参数localeContext设置为当前线程的LocaleContext,并设置参数threadContextInheritable为true,表示上下文对象可以在子线程中继承
    if (localeContext != null) {
        LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
    }
    // 将参数requestAttributes设置为当前线程的RequestAttributes,并设置参数threadContextInheritable为true,表示上下文对象可以在子线程中继承
    if (requestAttributes != null) {
        RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
    }
}

(Ps:RequestAttributesSpring框架中的一个接口,用于表示一个请求的属性集合。它允许应用程序在线程中存储和访问请求的属性,这些属性可以用于在请求的不同阶段共享数据或传递数据。)

而在RequestContextHolder内部的setRequestAttributes方法中,其会根据inheritable属性的不同来将request属性选择性的放入requestAttributesHolderinheritableRequestAttributesHolder两个不同的ThreadLocal

而两者的区别在于子线程是否可以共享父线程属性而默认情况下inheritable的取值为false,也就是说在SpringMVC默认情况下requestAttributes是不会线程共享的

(Ps:此处的RequestAttributes是我们之前提及ServletRequestAttributes的父接口)

进一步,setRequestAttributes的内部逻辑如下:

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中,如果我们想获取当前请求的Request对象,通常我们会通过RequestContextHolder进行获取。进一步RequestContextHolder获取Request对象需要先获取ServletRequestAttributes对象,进而通过其getRequest方法来获取到当前的请求信息。

换言之,如果想获取当前请求的Request对象,我们首先需要确保能获取到ServletRequestAttributes这一中间信息,因为其内部会维护相关的请求对象。而在SpringMVC内部ServletRequestAttributes在保存在RequestContextHolder中的ThreadLocal

而默认情况下,其实不支持父子线程间传递的,所以在多线程环境下当我们通过RequestContextHolder获取请求时会出现请求无法获取的现象,而导致这一问题本质发生的本质原因在于ServletRequestAttributes并未实现父子线程间的共享!

以上就是SpringMVC多线程下无法获取请求的原因和解决方法的详细内容,更多关于SpringMVC无法获取请求的资料请关注脚本之家其它相关文章!

相关文章

  • java中对象的强、软、弱、虚四种引用详解

    java中对象的强、软、弱、虚四种引用详解

    这篇文章主要介绍了java中对象的强、软、弱、虚四种引用详解,对象的引用分为4种,分别是强引用>软引用>弱引用>虚引用,程序员可以通过不同的引用控制对象的生命周期,方便垃圾回收,使程序更加灵活的控制对象生命周期,需要的朋友可以参考下
    2023-09-09
  • Spring Security过滤器链体系的实例详解

    Spring Security过滤器链体系的实例详解

    这篇文章主要介绍了Spring Security过滤器链体系,通过思维导图可以很好的帮助大家理解配置类的相关知识,结合实例代码给大家介绍的非常详细,需要的朋友可以参考下
    2022-02-02
  • SpringBoot整合SpringSecurity认证与授权

    SpringBoot整合SpringSecurity认证与授权

    在项目开发中,权限认证是很重要的,尤其是一些管理类的系统,对于权限要求更为严格,本文主要介绍了SpringBoot整合SpringSecurity认证与授权,感兴趣的可以了解一下
    2023-11-11
  • MyBatis中foreach标签的collection属性的取值方式

    MyBatis中foreach标签的collection属性的取值方式

    这篇文章主要介绍了MyBatis中foreach标签的collection属性的取值方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • Maven中jar包冲突原理与解决办法

    Maven中jar包冲突原理与解决办法

    这篇文章主要介绍了Maven中jar包冲突原理与解决办法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-09-09
  • java同步之如何写一个锁Lock

    java同步之如何写一个锁Lock

    本篇文章的目标一是自己动手写一个锁,这个锁的功能很简单,能进行正常的加锁、解锁操作。能更好地理解后面章节将要学习的AQS及各种同步器实现的原理。下面小编来和大家一起学习下吧
    2019-05-05
  • Java利用ZipOutputStream进行高效压缩的技巧详解

    Java利用ZipOutputStream进行高效压缩的技巧详解

    ZipOutputStream 是 Java 标准库中用于 创建 ZIP 文件 的核心类,位于 java.util.zip 包中,本文主要和大家介绍了Java如何使用ZipOutputStream进行高效压缩,有需要的可以了解下
    2025-07-07
  • Java让多线程按顺序执行的几种方法

    Java让多线程按顺序执行的几种方法

    本文主要介绍了Java让多线程按顺序执行的几种方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-05-05
  • IDEA 当前在线人数和历史访问量的示例代码

    IDEA 当前在线人数和历史访问量的示例代码

    这篇文章主要介绍了IDEA 当前在线人数和历史访问量的实例代码,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • SpringCloud解决Feign异步回调问题(SpringBoot+Async+Future实现)

    SpringCloud解决Feign异步回调问题(SpringBoot+Async+Future实现)

    这篇文章主要介绍了SpringCloud解决Feign异步回调问题(SpringBoot+Async+Future实现),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-11-11

最新评论