feign调用丢失请求头问题解决及原理分析

以为feign调用请求头里添加cookie为例!!!!添加其他请求头数据过程一样!!!

一、问题抛出

  • 我们有两个服务A、B,现在用户通过浏览器访问A服务的某个接口
  • 在该接口中通过openFeign调用了B服务
  • B服务中有个拦截器,对所有的请求都进行拦截,检查请求头中是否包含了cookie,如果有cookie这放行,如果没有cookie则进行拦截

问题:我们知道openFeign进行远程调用的时候他会新构建一个RequestTemplate进行请求,由于是新发起的请求,所以请求头里当然没有浏览器请求里带上的cookie数据了,如何让这个openFeign请求带上浏览器请求时的cookie呢?

二、问题分析

通过上面的介绍,我们知道openFeign进行远程调用的时候,由于是新起的一个请求调用,所以他丢失了原有请求(浏览器发起的)里的cookie信息以及请求头信息。

那么我们能不能将原有的请求里的cookie数据拿出来,然后在进行openFeign调用的时候在放入到新的请求中呢?当然可以,这就是我们下面介绍的feign拦截器原理。

三、问题解决

①、创建一个feign调用拦截器并注入容器

@Configuration
public class FeignConfig {

    @Bean("requestInterceptor")
    public static RequestInterceptor requestInterceptor() {
        // 创建拦截器
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                // 1、使用RequestContextHolder拿到原生请求的请求头信息(下文环境保持器)
                // 从ThreadLocal中获取请求头(要保证feign调用与controller请求处在同一线程环境)
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (requestAttributes != null) {
                    // 获取controller请求对象
                    HttpServletRequest request = requestAttributes.getRequest();
                    // 如果使用线程池进行远程调用,则request是空的(因为RequestContextHolder.getRequestAttributes()是从threadlocal里拿的值)
                    if (Objects.nonNull(request)) {
                        //2、获取老请求里的cookie信息
                        String cookie = request.getHeader("Cookie");
                        // 同步Cookie (将老请求里的cookie信息放入新请求里(RequestTemplate))
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
    }
}

②、测试

再次启动远程调用,此时就会发现feign调用的请求头里有了cookie数据

四、源码分析

通过上面的添加一个RequestInterceptor拦截器就解决了feign调用丢失请求头里的数据问题,那么他的做到的呢?原理是什么呢?其实去看一眼feign调用的源码就知道了,很简单

首先要知道feign调用底层原理是基于动态代理!!!

1、FeignInvocationHandler.invoke()

通过debug我们可以看到,feign调用最终会执行FeignInvocationHandler.invoke()方法

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();
  }
	// 这里是具体执行逻辑,点进invoke
  return dispatch.get(method).invoke(args);
}

2、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) {
        // 省略一部分代码。。。。这里其实就是做失败重试,和我们的主题没有关系
      continue;
    }
  }
}

3、SynchronousMethodHandler.executeAndDecode()

  • 可以看到他是先构建请求用的Request
  • 然后再通过client.execute(request, options);发起的真正调用
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
  // 1、构建request,核心就在这里了,点进去
  Request request = targetRequest(template);

  Response response;
  long start = System.nanoTime();
  try {
    // 2、真正发起调用的地方
    response = client.execute(request, options);
    response = response.toBuilder()
        .request(request)
        .requestTemplate(template)
        .build();
  } catch (IOException e) {
    throw errorExecuting(request, e);
  }
  // 省略部分无关代码
}

4、SynchronousMethodHandler.targetRequest()

Request targetRequest(RequestTemplate template) {
  // 这里也就是我们自定义RequestInterceptor被调用的地方
  // 可以看到,他拿到容器中所有实现了RequestInterceptor接口的bean,然后依次执行他们的apply方法
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}

5、总结

通过上面跟了一波源代码,我们知道了为openFeign增加请求头数据的工作原理,如下:

  • 注入一个自定义的RequestInterceptor接口的实现类到容器中
  • openFeign发起调用前会先构建请求的Request,在构建request的时候就会拿到我们自定义的RequestInterceptor并执行,在自定义的RequestInterceptor实现类中便会从原有请求里拿到请求头信息,然后放入feign调用的request里

注意如果是通过线程池或多线程进行发起feign调用,那么通过上述的添加RequestInterceptor实现类里通过RequestContextHolder.getRequestAttributes()获取原有请求里的请求头数据是获取不到的,这种情况怎么解决?其实也简单,最简单的方式就是在提交任务到线程池里的时候将请求头信息传递进去就可以了。也可以使用阿里的TransmittableThreadLocal.

你可能感兴趣的:(原理,/,源码,java,servlet,http)