深入剖析SpringMVC多线程下无法获取请求的原因


思考,输出,沉淀。用通俗的语言陈述技术,让自己和他人都有所收获。

作者:毅航😜


从问题出发,逐步讲透在SpringMVC中使用RequestContextHolder对象在多线程情况下无法获取请求的真实原因。

前言

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

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

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

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

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

问题复现

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

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

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

java 复制代码
@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属性,方法内部具体逻辑如下所示:

java 复制代码
    /**
     * 获取客户端请求头中的语言信息。
     * 其会从当前的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内部逻辑如下所示:

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

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

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

追踪溯源

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

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

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

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

java 复制代码
@Override
protected void service(HttpServletRequest request, 
                        HttpServletResponse response)
        {

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

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

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

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

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

java 复制代码
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的内部逻辑如下:

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

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

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

相关推荐
wyh要好好学习29 分钟前
SpringMVC快速上手
java·spring
尢词31 分钟前
SpringMVC
java·spring·java-ee·tomcat·maven
wrx繁星点点41 分钟前
享元模式:高效管理共享对象的设计模式
java·开发语言·spring·设计模式·maven·intellij-idea·享元模式
2401_865854881 小时前
iOS应用想要下载到手机上只能苹果签名吗?
后端·ios·iphone
AskHarries2 小时前
Spring Boot集成Access DB实现数据导入和解析
java·spring boot·后端
2401_857622662 小时前
SpringBoot健身房管理:敏捷与自动化
spring boot·后端·自动化
程序员阿龙2 小时前
基于SpringBoot的医疗陪护系统设计与实现(源码+定制+开发)
java·spring boot·后端·医疗陪护管理平台·患者护理服务平台·医疗信息管理系统·患者陪护服务平台
前 方2 小时前
若依入门案例
java·spring boot·maven
程思扬2 小时前
为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
linux·服务器·网络·经验分享·后端·网络协议·1024程序员节
阿华的代码王国2 小时前
【Spring】——SpringBoot项目创建
java·spring boot·后端·启动类·target文件