目录
- 前言
- 入坑一
- 出坑一
- 入坑二
- 出坑二
- 入坑三
- 出坑三
- 拓展
- 总结
前言
小伙伴们是不是会很纳闷,获得头部参数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>
总结
本文老顾介绍了头部参数传递的问题,在不同的应用场景中会产生不同的问题;希望能够帮助到小伙伴;谢谢!!!
---End---
老顾的微服务网关分享课程,请大家多多支持
推荐阅读
5分钟让你理解K8S必备架构概念,以及网络模型(一)
5分钟让你理解K8S必备架构概念,以及网络模型(二)
大厂如何基于binlog解决多机房同步mysql数据(一)?
大厂如何基于binlog解决多机房同步mysql数据(二)?
基于binlog的canal组件有哪些使用场景(三)?
基于binlog日志之canal企业应用及高可用原理(四)?
可用于大型应用的微服务生态灰度发布如何实现?
一线大厂级别公共Redis集群监控,细化到每个项目实例
Sharding-jdbc的实战入门之水平分表(一)
Sharding-Jdbc之水平分库和读写分离(二)
a、dubbo如何处理业务异常,这个一定要知道哦!
b、企业级SpringBoot应用多个子项目配置文件规划、多环境支持(一)
c、企业级SpringBoot应用多个子项目配置文件规划、多环境支持(二)
d、企业级SpringBoot应用多个子项目配置文件之配置中心(三)
e、利用阿里开源工具进行排查线上CPU居高问题
f、阿里二面:如何快速排查死锁?如何避免死锁?
g、微服务分布式架构中,如何实现日志链路跟踪?
h、网关如何聚合各个微服务的接口文档?
i、Kubernetes之POD、容器之间的网络通信
j、K8S中的Service的存在理由
k、企业微服务项目如何进入K8S的全过程
l、阿里开源项目Sentinel限流、降级的统一处理
m、大厂二面:Redis的分布式布隆过滤器是什么原理?
1、基于RocketMq的SpringCloud Stream框架实战入门
2、如何搭建消息中间件应用框架之SpringCloud Stream
3、面试必备:网关异常了怎么办?如何做全局异常处理?
4、Gateway网关系列(二):SpringCloud Gateway入门实战,路由规则
5、Gateway网关系列开篇:SpringCloud的官方网关Gateway介绍
6、API网关在微服务架构中的应用,这一篇就够了
7、学习Lambda表达式看这篇就够了,不会让你失望的哦(续篇)
8、Lambda用在哪里?几种场景?
9、为什么会出现Lambda表达式,你知道吗?
10、不说“分布式事务”理论,直接上大厂阿里的解决方案,绝对实用
11、女程序员问到这个问题,让我思考了半天,Mysql的“三高”架构
12、大厂二面:CAP原则为什么只能满足其中两项?而不能同时满足
13、阿里P7二面:聊聊零拷贝的原理
14、秒杀系统的核心点都在这里,快来取
15、你了解如何利用token方式实现分布式Session吗?
16、Mysql索引结构演变,为什么最终会是那个结构呢?让你一看就懂
17、一场比赛涉及到的知识,用通俗易通的方式介绍并发协调
18、企业实战Redis全方面思考,你思考了吗?
19、面试题:Thread的start和run的区别
20、面试题:什么是CAS?CAS的作用以及缺点
21、如何访问redis中的海量数据?避免事故产生
22、如何解决Redis热点问题?以及如何发现热点?
23、如何设计API接口,实现统一格式返回?
24、你真的知道在生产环境下如何部署tomcat吗?
25、分享一线互联网大厂分布式唯一ID设计 之 snowflake方案
26、分享大厂分布式唯一ID设计方案,快来围观
27、你想了解一线大厂的分布式唯一ID生成方案吗?
28、你知道如何处理大数据量吗?(数据拆分篇)
29、如何永不迁移数据和避免热点? 根据服务器指标分配数据量(揭秘篇)
30、你知道怎么分库分表吗?如何做到永不迁移数据和避免热点吗?
31、你了解大型网站的页面静态化吗?
32、你知道如何更新缓存吗?如何保证缓存和数据库双写一致性?
33、你知道怎么解决DB读写分离,导致数据不一致问题吗?
34、DB读写分离情况下,如何解决缓存和数据库不一致性问题?
35、你真的知道怎么使用缓存吗?
36、如何利用锁,防止缓存击穿?重构思想的重要性
37、海量订单产生的业务高峰期,如何避免消息的重复消费?
38、你知道如何保障生产端100%消息投递成功吗?
39、微服务下的分布式session该如何管理?
40、阿里二面:filter、interceptor、aspect应如何选择?很多人中招
41、互联网架构重要组员CDN,很多高级开发都没有实操过,来看这里
42、阿里二面:CDN缓存控制原理,看看能不能难住你
本文暂时没有评论,来添加一个吧(●'◡'●)