java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringMVC请求头获取失败

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

作者:毅航

这篇文章主要介绍了我们就对多线程环境下使用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请求头获取失败的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文