网站首页 > 教程分享 正文
目录
- 前言
- 入坑一
- 出坑一
- 入坑二
- 出坑二
- 入坑三
- 出坑三
- 拓展
- 总结
前言
小伙伴们是不是会很纳闷,获得头部参数header,不就是从request对象中获取头部参数吗?
Enumeration 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远程调用获取头部参数;其实只要是父子线程之间共享值,都可以借鉴文章中提到的方案。尤其推荐阿里的组件,此组件还是蛮强大的。有兴趣的小伙伴可以去研究一下。
com.alibaba
transmittable-thread-local
2.11.4
总结
本文老顾介绍了头部参数传递的问题,在不同的应用场景中会产生不同的问题;希望能够帮助到小伙伴;谢谢!!!
---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缓存控制原理,看看能不能难住你
猜你喜欢
- 2025-03-24 每日分享- web api 项目有哪些接口远程调用的方法
- 2025-03-24 未来的热门!WEB APP与原生APP有哪些交互设计区别?
- 2025-03-24 从 node.js Web应用中调用 WASM 函数 | WebAssembly 入门教程
- 2025-03-24 C# + .NET5 Web入门实战:私人笔记(6)打通任脉
- 2025-03-24 复古写法,webservice代码编写主要包括服务器端发布和客户端调用
- 2025-03-24 Linux下C++访问web—使用libcurl库调用http接口发送解析json数据
- 2025-03-24 再见 Feign!推荐一款微服务间调用神器,跟 SpringCloud 绝配
你 发表评论:
欢迎- 最近发表
-
- 有了这份900多页的Android面试指南,你离大厂Offer还远吗?
- K2 Blackpearl 流程平台总体功能介绍:常规流程功能
- 零基础安卓开发起步(一)(安卓开发入门视频)
- 教程:让你的安卓像Windows一样实现程序窗口化运行
- Android事件总线还能怎么玩?(事件总线有什么好处)
- Android 面试被问“谈谈架构”,到底要怎样回答才好?
- Android开发工具Parcel和Serialize
- Android 中Notification的运用(notification widget安卓)
- Android退出所有Activity最优雅的方式
- MT管理器-简单实战-去除启动页(mt管理器怎么去除软件弹窗)
- 标签列表
-
- css导航条 (66)
- sqlinsert (63)
- js提交表单 (60)
- param (62)
- parentelement (65)
- jquery分享 (62)
- check约束 (64)
- curl_init (68)
- sql if语句 (69)
- import (66)
- chmod文件夹 (71)
- clearinterval (71)
- pythonrange (62)
- 数组长度 (61)
- javafx (59)
- 全局消息钩子 (64)
- sort排序 (62)
- jdbc (69)
- php网页源码 (59)
- assert h (69)
- httpclientjar (60)
- postgresql conf (59)
- winform开发 (59)
- mysql数字类型 (71)
- drawimage (61)
本文暂时没有评论,来添加一个吧(●'◡'●)