你是否也遇到了Feign埋的这个坑呢?#远程调用请求头丢失

发布时间 2023-04-03 21:48:16作者: LuRun

今天在我的电商项目的订单服务中遇到了两个问题:

  • 远程调用丢失请求头
  • 空指针异常

事情的经过是这样的:

​ 结算购物车中的商品创建一个订单时,首先要判断用户的登录状态,如果用户未登录的话就不能创建订单,而用户的登录状态信息在发起创建订单请求的时候保存在了 ThreadLocal 中,很容易就获取到了。接下来远程调用了购物车服务来查询该商品的相关信息,以及远程调用了会员服务来查询用户的收货地址,将这些信息封装为一个订单Vo然后提交,一整套操作下来简简单单非常nice,信心满满地启动了项目。

​ 就在我重新启动项目之后,发现,咦,明明我已经是登录状态了,但是订单却创建失败了,我几乎没有想到问题就出在登录状态信息上,我看了半天代码也没有找到原因,然后开启了疯狂DeBug模式,疯狂打断点,终于,在某一个时刻,我发现,调用远程服务时竟然判断我是未登录状态 ??????难道是远程调用这里出问题了?

​ 接下来又开启了疯狂的面向百度编程,浏览各大网站,查看了大佬们对这个问题的办法之后,我有了思绪,我决定深入Feign底层看一看,远程调用时究竟发生了什么。

呈上源码

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }

远程调用时首先经过一系列判断,判断是否需要调用,如果需要就进入 dispatch.get(method).invoke(args)

 @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template);
      } 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;
      }
    }
  }

这里拿到了一个重试器,如果成功的话就不需要重试,直接进入 executeAndDecode(template)

 Request request = targetRequest(template);
    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }
    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }

这个方法首先要传入一个请求模板 template ,template中声明了请求地URL地址以及请求头,根据这个模板得到了一个请求

Request request = targetRequest(template);

我们来看看这个请求是如何创建的

Request targetRequest(RequestTemplate template) {
    for (RequestInterceptor interceptor : requestInterceptors) {
      interceptor.apply(template);
    }
    return target.apply(template);
  }

发现这里使用了 RequestInterceptor 拦截器中的 apply() 方法去构建请求,而默认构建出来的请求是空的,也就是说这个请求根本就没有获取到我们主线程ThreadLocal用户登录信息。

所以自定义一个 RequestInterceptor ,去重写 apply() 方法,将我们主线程 ThreadLocal 中的用户登录信息保存在 Feign 创建的这个远程调用请求中

下面是我自定义的 Requestinterceptor

@Configuration
public class MyFeignConfig {
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        RequestInterceptor requestInterceptor = new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (requestAttributes != null) {           
                    HttpServletRequest request = requestAttributes.getRequest();
                    if (request != null) {                    
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
        return requestInterceptor;
    }
}

​ 有了自定义的这个 Requestinterceptor ,在远程调用时,就使用了这个 Requestinterceptor 来创建远程调用请求,从而解决了远程调用时请求头丢失的问题。

​ 接下来我想对这两个查询任务做个优化,所以创建了一个线程池,对这两个远程调用的查询任务使用异步执行的方式去做,然后获取到这两个异步任务执行的结果返回,这样几乎可以节省一半的查询时间。可是在就我重新启动项目时,发现报了空指针异常??????(心态崩了!)

​ 为什么呢?查看了报错信息,发现是在自定义的 RequestInterceptor 重写的apply() 方法中获取cookie时空指针了。也就是这行代码:

String cookie = request.getHeader("Cookie");

​ 想来简单,原因是 ThreadLocal 是不同线程之间不共享的,所以在使用线程池异步执行远程调用时不能获取到主线程ThreadLocal 中保存的用户登录信息。

​ 这还不简简单单,直接在每一个异步任务中重新获取一下主线程 ThreadLocal 中的信息不就OK了么。

又是饱经风霜的一天,明天加油!