Feign请求头丢失的解决方法及其分析

发现问题

在写多服务互相调用的时候,发现远程feign调用方法正常情况下是无法将请求头的信息(例如token等)顺带传播的。

我们可以添加远程 feign 远程调用拦截器,来获取token 数据。
在这里插入图片描述

如上图:因为微服务之间并没有传递头文件,所以我们可以定义一个拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的用户信息再放入到header中,再调用其他微服务即可。

package com.atguigu.tingshu.common.feign;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

@Component
public class FeignInterceptor implements RequestInterceptor {

    public void apply(RequestTemplate requestTemplate){
        //  获取请求对象
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if(null != requestAttributes) {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes;
            HttpServletRequest request = servletRequestAttributes.getRequest();
            String token = request.getHeader("token");
            requestTemplate.header("token", token);
        }
    }
}

我们再次清晰地梳理一下feign请求头丢失的问题:

原因:每次feign进行远程调用,会new一个新的Request,并发出去。不会看以前的老请求。

解决:利用拦截器;每次发请求之前。把老请求的请求头获取到放到RequestTemplate中,保证RequestTemplate有头信息就行。

PS:

同一个线程期间共享数据

  • Map<Thread,数据>: 手动移除
  • ThreadLocal<数据>: 可自动,推荐手动
  • RequestContextHolder
    • 请求上下文。能获取到之前的老请求; 原理ThreadLocal机制,把用户信息隐式传参下去。

feign进行远程调用的源码分析

对feign整体流程的分析。

  • 我们编写feignclient接口,SpringBoot容器启动会扫描每个接口并生成代理对象放在容器中
  • 自动注入feign接口类,会拿到它的代理对象; ReflectiveFeign.FeignInvocationHandler

1、SpringBoot启动扫描每个feign接口,并创建ReflectiveFeign 代理对象

2、并且把feign接口的每个方法,用什么处理器执行,提前保存起来

Map<Method, MethodHandler> dispatch;

​ 当前方法对应的MethodHandler是 SynchronousMethodHandler

3、远程调用流程是在: SynchronousMethodHandler.invoke(); 方法详细如下

  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }

详细流程:

  • 1、创建请求模板; 封装准备要发送的请求的完整数据(注意观察,头里面没东西);新new的

    • 在这里插入图片描述
  • 2、发现参数位置是否有连接超时等设置参数。(我们没有,连接超时都是统一配置)

    • 允许我们通过传参的方式精确调整每个请求的超时时间

    • //声明
      @GetMapping("/add/{Id}/{num}")
          Result<SkuInfo> addToCart(@PathVariable("Id") Long id,
                                    @PathVariable("num") Integer num,
                                    Request.Options options);
      
      //调用
      Request.Options options = new Request.Options(1000,2000);
      Result<Thing> result = testFeignClient.addThing(id, num,options);
      
  • 3、获取重试器: 利用while(true),判断重试器如果抛了异常,while炸掉就不重试,重试器没抛异常,while就会continue

    • while (true) {
            try {
                //真正执行远程调用
              return executeAndDecode(template, options);
            } catch (RetryableException e) {
              try {
                retryer.continueOrPropagate(e);
              } catch (RetryableException th) {
                Throwable cause = th.getCause();
                if (propagationPolicy == UNWRAP && cause != null) {
                  throw cause;
                } else {
                  throw th;
                }
              }
              if (logLevel != Logger.Level.NONE) {
                logger.logRetry(metadata.configKey(), logLevel);
              }
              continue;
            }
          }
      
  • 4、远程调用流程: executeAndDecode

    • 1、根据请求模板得到请求对象(注意观察,没头)

    • 2、client执行请求,开始远程调用

      • 1、找nacos要到需要调用的微服务的远程地址

      • 2、打开连接,发送请求

      • 在这里插入图片描述

      • 3、获取inputstream、outputstream数据

    • 3、把远程调用得到的json结果,利用decoder解码成方法希望的返回对象

feign是怎么进行远程调用

1、我们编写feign接口说清楚要调用哪个服务哪个地址@FeignClient

2、SpringBoot启动为每个接口创建代理对象

3、远程调用是代理对象在执行: 最终是 SynchronousMethodHandler帮我们远程调用

4、先解析各种参数;构造出远程请求对象。(请求方式、请求路径、请求参数、请求头)

5、利用LoadBalancerFeignClient负载均衡客户端先去nacos拿到对方微服务的所有地址

​ 有缓存机制,如果以前调用过,其他被调用过的服务的 FeignLoadBalancer 会被缓存起来

6、根据这个地址构造出URL。然后openConnection();打开连接准备收发数据

7、用connection的outputstream把我们要发出去的数据写出去

8、用connection的inputstream感知对方给我们响应的数据

9、利用decoder把对方响应给我们的数据,逆转为方法的返回值类型

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值