首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >feign远程调用丢失请求头源码分析与解决

feign远程调用丢失请求头源码分析与解决

作者头像
vivi
发布2021-01-26 16:23:49
2K0
发布2021-01-26 16:23:49
举报
文章被收录于专栏:vblogvblog

前言

我们在写服务端项目的时候,总会限制对某些资源的访问,最常见的就是要求用户先登录才能访问资源,当用户登录后就会将此次会话信息保存进session,同时返回给浏览器指定的cookie键值,下次浏览器再次访问,请求头中就会携带这个cookie,我们也以次来识别用户的登录状态,做出正确响应。

问题

有时候,我们先行登录,然后访问服务A的某个方法,请求头中携带cookie,标识我们已经登录。但若是我们访问的目标方法在执行过程中使用feign进行原程调用服务B(假设不存在跨域),而服务B也要先判断登录状态,我们可能发现服务B会调用失败,或者说拿不到数据,理由是服务B认为我们并未登录。而这时,如果我们直接从浏览器访问服务B的这个方法却能得到一个成功的响应。

也就是说:

浏览器--->服务A成功; 服务A-->服务B失败; 浏览器-->服务B失败 结合上面所说,服务AB都会先判断用户登录状态,浏览器直接访问AB时都会带上登录成功后保存的cookie,而服务A通过Feign远程调用B,却被认为未登录,显然,这部分请求头数据丢失。

feign源码分析

我们来看下feign远程调用是如何执行的,我们在feign远程调用之处打上断点

在这里插入图片描述
在这里插入图片描述
  1. step into进入方法执行,会发现是一个代理对象的invoke方法在执行,首先判断是方法名,
  • 如果是 toString(),hashCode(),equals()这几个方法,那就是本地直接完成了。
  • 如果是真正的远程调用,就会最后进入最后一行。
在这里插入图片描述
在这里插入图片描述
  1. 上一步之后再次step into,发现还是一个invoke方法,方法内,首先根据请求参数创建一个RequestTemplate,核心部分是 while(true) 里面的 executeAndDecode(),while 其实是加了一层重试机制,这里不多说。
在这里插入图片描述
在这里插入图片描述
  1. 进入executeAndDecode方法,我们看到 targetRequest() 构造出了一个request对象,而最终的response就是这个request请求的执行结果。
在这里插入图片描述
在这里插入图片描述

同时我们能够看到这个request对象的请求头中是空的,当然也就不存在cookie,也就无法识别我们是否登录。

在这里插入图片描述
在这里插入图片描述

小结

  • 使用feign进行远程调用时,首先判断目标方法类型,如果是 toString(),hashCode(),equals()这几个方法,那就是本地直接完成了;
  • 如果是真正的远程调用,执行的是 executeAndDecode 方法,在这个方法体内,会通过 targetRequest 方法创建出一个新的 request 对象,这个新的request会按照我们指定的参数和路径去发送请求,并获得响应结果。
  • 这个新的request对象的请求头为空(所以会丢失原来的请求头)

解决

问题在于feign自己创建出resttemplate,再用它构建一个新的request对象去发送请求,而这个新的request不包含任何请求头信息。我们应该在它创造出这个request之后,在它真正发送请求之前,把原始请求头中的数据给它复制过去。

我们来看一下feign最后构建出创建request对象的 targetRequest方法

在这里插入图片描述
在这里插入图片描述

我们发现这里面会有调用了一系列 RequestInterceptorapply方法对其进行增强,最后才返回,只不过默认情况下这些拦截器是空的。

因此 ,我们需要需要自己实现一个 RequestInterceptor,在它的apply方法中将原始请求头中的数据同步到feign创建出的新的request中,并且将这个拦截器注入容器中,这样feign在执行目标方法之前会被其拦截,对其先进行增强。

@Component
public class FeignBeforeExecInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        // 拿到原始请求头数据
        String cookie = request.getHeader("Cookie");
        if (!StringUtils.isEmpty(cookie)) {
            // 同步
            template.header("Cookie", cookie);
        }
    }
}

比较难处理的地方在于,我们如何拿到原始的request对象,spring提供了一个叫 RequestContextHolder对象帮我们解决这个难题,通过它的 getRequestAttributes 方法或者 currentRequestAttributes 方法就能获取到原始请求数据。关于这两个方法的区别,可简单认为,前者如果获取失败,会返回null;而后者会抛出异常。

在这里插入图片描述
在这里插入图片描述

关键

还有个问题是这个 RequestContextHolder是如何保存原始请求的,以至于我们在任何时候都能很方便的拿到,而不是像只能在controller层通过方法参数获取。其实如果你细心看上面的源码图片中的注释的话,就能看到它写的是获取与当前线程绑定的请求数据

我们知道,服务器(tomcat)会为每一个请求分配一个线程,从filter到controller到service到db再返回,全都都是同一个线程,所以,只要从一开始就把原始请求和这个线程绑定在一起,那么只要在这个线程内,我就能随时拿到这个数据。

是不是很熟悉,这不就是ThreadLocal嘛!再瞅一眼源代码证明一下?

在这里插入图片描述
在这里插入图片描述

总结

  • feign远程调用,自己创建一个新的request对象,按照指定的路径和参数发起新的请求,并得到响应结果。但是这个新的request对象请求头为空,所以丢失了原先请求中的数据。
  • feign在创建新的request对象时,会调用一系列容器中的RequestInterceptor对象,执行其apply方法,对这个创建好的request进行增强,再去真正执行请求。但是默认情况下容器中不存在这类拦截器对象。
  • 我们可以自己向容器中注册一个RequestInterceptor,在其apply方法体内,获取到原始request,将其数据取出,赋值到新的request中,完成请求头的同步。
  • RequestContextHolder借助ThreadLocal将每一个原始请求与tomcat为其分配的线程绑定,之后,只要在同个线程内,随时随地都可轻易获取到原始request。

注意事项

异步编排下,上述解决方案失效,请求头仍丢失
  • cartFeignService.getCheckedItems(); 写在位置一,上述解决方案没问题
  • cartFeignService.getCheckedItems(); 写在位置二,请求头仍为空
    @Override
    public OrderConfirmVO confirmOrder() {
        MemberInfoVO loginUser = LoginInterceptor.threadLocal.get();
        OrderConfirmVO orderConfirmVO = new OrderConfirmVO();
        // cartFeignService.getCheckedItems();           ------位置一
        // 异步编排,会开启新的线程
        CompletableFuture<Void> orderItemsTask = CompletableFuture.runAsync(() -> {
            // 查询购物车
            R res = cartFeignService.getCheckedItems();  ------位置二
            List<OrderItemTO> itemTOS = res.getData(new TypeReference<List<OrderItemTO>>() {
            });
            orderConfirmVO.setItems(itemTOS.stream().map(to -> convertOrderItemTO2OrderItemVO(to)).collect(Collectors.toList()));
        });

请注意,我们的解决方法的关键之处在于,在RequestInterceptorapply方法体内拿到原始request,拷贝属性到新的request。

而我们是在apply方法体内,通过 RequestContextHolder.getRequestAttributes() 获取的。RequestContextHolder是借助ThreadLocal将每一个原始请求与tomcat为其分配的线程绑定,之后,只要在同个线程内,随时随地都可轻易获取到原始request。

所以如果你的feign调用出现在异步线程体内,RequestInterceptor拦截到你时,你再使用RequestContextHolder,获取的已经不是原来线程,必然无法获取到原请求,只能拿到与当下线程绑定的request,甚至直接得到一个null,引发空指针异常。

解决

这要如何解决?很简单,提前同步数据。进入新线程之前,拿出原线程绑定的requestAttributes,在新的线程体内,feign调用之前,将其赋值到本线程绑定的request中,这样,在执行feign方法,被拦截器拦截时,当前线程绑定的request已不为空。(RequestContextHolder写在哪就绑定的是哪个线程)

    @Override
    public OrderConfirmVO confirmOrder() {
        MemberInfoVO loginUser = LoginInterceptor.threadLocal.get();
        OrderConfirmVO orderConfirmVO = new OrderConfirmVO();
        // 原线程绑定的requestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 异步
        CompletableFuture<Void> orderItemsTask = CompletableFuture.runAsync(() -> {
            // 赋值到新线程绑定的request
            RequestContextHolder.setRequestAttributes(requestAttributes);
            // 异步查询购物车
            R res = cartFeignService.getCheckedItems();
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-01-19 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 问题
  • feign源码分析
  • 小结
  • 解决
  • 关键
  • 总结
  • 注意事项
    • 异步编排下,上述解决方案失效,请求头仍丢失
      • 解决
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档