全部分类

您的位置:>首页 >技术文章

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

作者: 时间:2021-03-03 13:22:08 点击量:407 来源:https://www.toutiao.com/i6928950917370888717

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

目录

  1. 前言
  2. 入坑一
  3. 出坑一
  4. 入坑二
  5. 出坑二
  6. 入坑三
  7. 出坑三
  8. 拓展
  9. 总结

前言

小伙伴们是不是会很纳闷,获得头部参数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>

总结

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

点击这里给我发消息