计算机系统应用教程网站

网站首页 > 技术文章 正文

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

btikc 2024-09-10 12:03:14 技术文章 11 ℃ 0 评论

目录

  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>

总结

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

---End---


老顾的微服务网关分享课程,请大家多多支持

推荐阅读

5分钟让你理解K8S必备架构概念,以及网络模型(一)

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面试必备:网关异常了怎么办?如何做全局异常处理?

4Gateway网关系列(二):SpringCloud Gateway入门实战,路由规则

5Gateway网关系列开篇:SpringCloud的官方网关Gateway介绍

6API网关在微服务架构中的应用,这一篇就够了

7学习Lambda表达式看这篇就够了,不会让你失望的哦(续篇)

8Lambda用在哪里?几种场景?

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缓存控制原理,看看能不能难住你

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表