你所不知道的头部参数传递的坑,来吧!抓紧出坑!

4,674 阅读8分钟

前言

小伙伴们是不是会很纳闷,获得头部参数header,不就是从request对象中获取头部参数吗?

Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
    //头部参数名
    String name = headerNames.nextElement();
    //头部参数值
    String value = request.getHeader(name);
}

上面的代码就能够获取头部参数了哦。如果你是这么认为的,通过这篇文章你会重新认识,会一步步带着你入坑,让你知道头部参数获得,是如此之难;然后再领着你出坑,让你的认知提升几个台阶。

入坑一

我们先来一个简单的坑,也是只要从事微服务架构开发的人,应该都会遇到的坑。

图片

上图中是微服务架构中,常见的业务,即consumer服务调用provider服务,那用户调用consumer服务的时候传入header参数,那到最后的provider服务这里能否获得到呢?

consumer消费端代码:

图片

provider提供端代码:

图片

consumer消费端中Feign代码:

@FeignClient(name = "service-provider")
public interface ProviderServiceFeign {
    @GetMapping("/transferHeaders")
    public String transferHeaders();
}

上面的代码表示了客户端传入deviceId和token参数,需要在consumer和provider两个服务都能够获取到,然后打印出来。那我们启动测试一下

图片

执行结果

consumer消费端打印结果如下,能够获取到header参数

c.p.q.e.controller.ConsumerController: consumer服务中获取的请求头deviceId==1111 ,token==2222

provider生产端打印结果如下,发现没有获取到header参数

c.p.q.e.controller.ProviderController: provider服务中从请求头中获取的deviceId===null
c.p.q.e.controller.ProviderController: provider服务中从请求头中获取的token===null

那为什么没有获取到呢?这个原因是Feign调用是个远程RPC调用,虽然底层是通过httpClient方式去调用的,但是它并没有把原始的header参数传入, 那怎么出这个坑呢?怎么改呢?

出坑一

Feign提供了一个RequestInterceptor请求拦截器,我们只要在feign调用之前把header参数传入就可以了。

如下代码:

图片

上面代码就是实现RequestInterceptor接口的apply方法,参数template中有个对header封装,只需要在feign调用之前把header参数值传入到template中就ok了,这样就顺利把header参数传递了。

上面代码我们只传递header参数deviceid和token的值;其他忽视 这个拦截器要在消费端进行注入加载哦,要做成公共的组件core包,给微服务引用就行

启动测试一下

消费端打印结果

c.p.q.e.controller.ConsumerController  : consumer服务中获取的请求头deviceId==1111 ,token==2222

生产端打印结果

c.p.q.e.controller.ProviderController  : provider服务中从请求头中获取的deviceId===1111
c.p.q.e.controller.ProviderController  : provider服务中从请求头中获取的token===2222

我们发现provider生产端能够正常获得header参数了。

入坑二

小伙伴是不是觉得**这样feign调用的header参数传递就没有问题的吗?**我们举个例子,如果consumer端在调用provider的时候,需要异步调用,也就是开启一个子线程去调用provider方法;

这个业务一般就是,如果provider方法耗时很长;导致consumer调用方耗时也长;那如果业务认可的情况下,我们可以不需要等待provider的执行结果,继续执行consumer就行了

看如下代码:

图片

启动测试,provider生产端打印结果,没有获取到header参数

c.p.q.e.controller.ProviderController  : provider服务中从请求头中获取的deviceId===null
c.p.q.e.controller.ProviderController  : provider服务中从请求头中获取的token===null

这个是为什么呢?我们来调试一下,发现FeignRequestInterceptor拦截器ServletRequestAttributes attributes为null,导致header参数传递失败。

图片

怎么会获取不到**ServletRequestAttributes呢?**这个就需要了解一下RequestContextHolder到底是什么?我们看一下源码

图片

图片

图片

我们发现RequestContextHolder本质是通过ThreadLocal进行变量的保存和获取的;也就是header参数值是保存在ThreadLocal中的。那客户端请求过来时,主线程对header参数保存到了主线程的ThreadLocal;但是如果子线程调用feign时,子线程是没法获得主线程的ThreadLocal的,所以获得为null。

原因知道了;那怎么解决呢?

出坑二

怎么解决上面的问题?**本质就是要解决子线程如何能够获取到父线程的ThreadLocal?**这边就出来了另一个ThreadLocal,即InheritableThreadLocal

看看他们之间的区别

•**ThreadLocal:**单个线程生命周期强绑定,只能在某个线程的生命周期内对ThreadLocal进行存取,不能跨线程存取。

InheritableThreadLocal:

(1)可以无感知替代ThreadLocal的功能,当成ThreadLocal使用。

(2)明确父-子线程关系的前提下,继承(拷贝)父线程的线程本地变量缓存过的变量,而这个拷贝的时机是子线程Thread实例化时候进行的,也就是子线程实例化完毕后已经完成了InheritableThreadLocal变量的拷贝,这是一个变量传递的过程。

那我们怎么修改呢?其实我们刚才看到的RequestContextHolder源码中,就有InheritableThreadLocal

图片

图片

从上面的源码中可以看到,我们把setRequestAttributes第二个参数为true就行了。

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);//请求属性可继承,线程共享

那我们在调用子线程的时候,调用此方法就行了;看一下修改的代码

图片

我们来看看provider服务的打印结果

图片

打印好像是正确了;provider服务是能够获取到header参数。

但是小伙伴们仔细看一下,下面有获取不到的情况

图片

这个时候 就出现了第三个坑,小伙伴继续往下看

入坑三

看到上面的问题,又测试了很多次,都会时不时的出现获取不到的情况

图片

这个是什么原因呢?**为什么时不时会获取不到呢?**这个问题就要涉及到比较底层方面的知识了。我们来梳理一下

1)provider服务是由consumer服务调用的,而且是子线程发起的

2)我们已经解决了子线程可以获得主线程的属性的问题

那为什么会出现上面的问题呢?本质原因就是主线程在子线程之前就结束了底层原理Servlet容器中Servlet属性生命周期与接收请求的用户线程(父线程)同步, 随着父线程执行完destroy()而销毁;

小伙伴就会问,InheritableThreadLocal不是已经把变量拷贝过来了吗?父线程销毁了应该不影响啊?肯定的给你回答是对的。

但是我们在看一下源码RequestContextHolder中setRequestAttributes方法

图片

在源码中ThreadLocal对象保存的是RequestAttributes attributes;这个是保存的对象的引用一旦父线程销毁了,那RequestAttributes也会被销毁,那RequestAttributes的引用地址的值就为null**;**虽然子线程也有RequestAttributes的引用,但是引用的值为null了。

我们再看一下consumer消费端的代码

图片

根据上面的原理,我们就会知道为什么有时候会得到header;有时候得不到了。因为有时候主线程会在子线程前结束。就会导致获取不到。

小伙伴们看到这里,应该明白原因了吧!本质原因找到了,那怎么解决呢?

出坑三

我们知道了上面问题的原因,就是父线程提前结束了,子线程还在运行时,那个时候获取不到header参数。怎么解决?我们来看看问题出现在子线程那边获取到的是对象的引用,不是具体的值。如果我们可以把值拷贝到子线程,那就可以解决此问题了。

知道了解决方案,那我们怎么设计呢?看下面的设计

图片

上图的核心思想就是把header参数放到另外的ThreadLocal变量中,不采用原生的RequestAttributes。上代码

图片

定义RequestHeaderHolder对象,作用就是保存线程本地变量,此代码引用了阿里的TTL组件TransmittableThreadLocal,大家可以认为就是个增强版的InheritableThreadLocal。

当然也可以采用原生的InheritableThreadLocal,在头部参数获取场景,是一样的。具体和阿里的有什么区别,不在此篇文章范围;下次介绍区别。

图片

上面代码就是请求拦截器把header参数,赋值到RequestHeaderHolder对象中;这样就保证了每次的请求头部header值都在RequestHeaderHolder里面。

注意:一定要在afterCompletion方法中remove值,要不然会有内存溢出的隐患。

把此请求拦截器需要注册到WebMvc里面,看下面的代码

图片

注意:此处一定要实现WebMvcConfigurer;而不是网上说的WebMvcConfigurationSupport;因为如果用WebMvcConfigurationSupport会有个坑 到底是什么坑?以后文章会介绍。

我们在来改造一下Feign请求拦截器

图片

核心思想就是不从之前的获取头部header参数

RequestContextHolder.getRequestAttributes();

改为从我们定义的RequestHeaderHolder对象里面获取。代码改到这里就结束了 我们来启动测试一下;再也没有出现过获取不到的情况了哦!

图片

拓展

上面都是介绍了Feign远程调用获取头部参数;其实只要是父子线程之间共享值,都可以借鉴文章中提到的方案。尤其推荐阿里的组件,此组件还是蛮强大的。有兴趣的小伙伴可以去研究一下。

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

总结

本文介绍了头部参数传递的问题,在不同的应用场景中会产生不同的问题;希望能够帮助到小伙伴;谢谢!!!

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包