Feign 远程调用丢失请求上下文问题

一、Feign 远程调用丢失请求头问题

1、业务场景:

  • 正常的微服务之间利用Feign远程调用,没有额外配置的情况下。
  • 假设 “远程调用A服务”时,在执行请求的时候,需要获取请求头携带的Cookie数据执行某操作。
  • 如下示例代码,执行后,发现A服务获取不到调用者的“请求头”信息。
	@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        //1、远程调用A服务 ,查某数据。 
        aFeignService.getAInfo(); //代码略...
        
        //2、远程调用B服务,查某数据。
        bFeignService.getBInfo(); //代码略...
        
        //3、远程调用C服务,查某数据。
        cFeignService.getCInfo(); //代码略...

        return orderConfirmVo;
    }

2、解决方案:

添加 feign.RequestInterceptor 拦截器的实现类。

Feign 远程调用丢失请求上下文问题_第1张图片

方案一:

  • 可以使用默认提供的拦截器,拦截器实现如下图。 没有添加 Cookie的拦截器实现,自己实现一个即可。

Feign 远程调用丢失请求上下文问题_第2张图片

方案二:

  • 自定义 Feign 拦截器,拦截后添加请求头信息。
import javax.servlet.http.HttpServletRequest;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import feign.RequestInterceptor;
import feign.RequestTemplate;

/**
 * @author Lay.He
 * @date 2022/6/19 13:16
 * @description Feign配置类
 */
@Configuration
public class FeignConfig {

    /**
     * 自定义 Feign 拦截器,添加请求头信息
     *
     * @return {@link RequestInterceptor}
     */
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                //获取 RequestAttributes (里面用到了 ThreadLocal,同一个线程可以共享RequestAttributes)。
                ServletRequestAttributes servletRequestAttributes
                    = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if (servletRequestAttributes != null) {
                    //获取当前请求的  HttpServletRequest request
                    HttpServletRequest request = servletRequestAttributes.getRequest();
                    //获取当前请求的数据,比如:Cookie
                    String cookie = request.getHeader("Cookie");
                    //同步 Cookie信息
                    template.header("Cookie", cookie);
                    //添加其他请求头信息
                    // template.header("xxx",...);
                }
            }
        };
    }
}

3、Feign调用源码分析:

final class SynchronousMethodHandler implements MethodHandler {
   //其他方法,略.... 
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
          //1、【入口】执行远程调用的目标方法 和 解码(转换对象)
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
       //略...
      }
    }
  }
    
    /* 1、【入口】执行远程调用的目标方法 */
    Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        //2、【重点】构造目标请求对象 
        Request request = targetRequest(template); 
       	// 略..
        
        Response response;
        long start = System.nanoTime();
        try {
            //3、 真正调用远程目标方法的代码
            response = client.execute(request, options);
            
            response = response.toBuilder()
                .request(request)
                .requestTemplate(template)
                .build();
        } catch (IOException e) {
            //.略
        }
        //其他处理逻辑..略
    }
    
      /* 2、【重点】构造目标请求对象方法  */
    Request targetRequest(RequestTemplate template) {
        //2.1 循环遍历,Feign的 请求拦截器 ——List ,默认是空的
        for (RequestInterceptor interceptor : requestInterceptors) {
            interceptor.apply(template);
        }
        //相当于 headers请求头没处理,是空的,这就是Feign调用请求头信息丢失的原因。解决办法需要自己添加拦截器。
        return target.apply(template);
    }

}    

【重点】构造目标请求对象 targetRequest()方法,会通过拦截器处理请求上下文。如果没有自定义拦截器,则默认的就是不处理请求头。

Feign 远程调用丢失请求上下文问题_第3张图片

二、Feign 异步编排情况下,丢失上下文问题

1、业务场景:

1.1 同一线程下,远程调用

【前置条件】

  • 完成 方案二:添加自定义Feign拦截器

  • 使用“同一下线程下,同步的远程调用A-B-C”,测试Feign远程调用;

【结果】

  • 每次远程调用前,都会被“添加的自定义拦截器”拦截,从RequestContextHolder.getRequestAttributes() 中能正常获取请求头数据。

示例代码类似如下:

	@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        //1、远程调用A服务 ,查某数据。 
        aFeignService.getAInfo(); //代码略...
        
        //2、远程调用B服务,查某数据。
        bFeignService.getBInfo(); //代码略...
        
        //3、远程调用C服务,查某数据。
        cFeignService.getCInfo(); //代码略...

        return orderConfirmVo;
    }

1.2 异步线程下,远程调用

【问题复现】

  • 由于多个远程调用之间没有联系,同步执行比较耗时,采取 CompletableFuture异步编排的方式可以加快查询速度。

  • 于是,采用如下的方式“多线程异步执行”,相当于异步的远程调用Feign;

【结果】

  • “异步线程A” 和 “异步线程B“ 两次利用Feing远程调用时,在自定义Feign的拦截器中RequestContextHolder.getRequestAttributes() 取值均为NULL,即异步执行情况下,Feign又再次丢失了上下文。
	@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        //主线程....
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        
        //启用异步编排的方式,别名: A 线程
        CompletableFuture<Void> aFuture = CompletableFuture.runAsync(() -> {
            //1、远程调用A服务 ,查某数据。
         	//代码略...
        }, executor);

        //启用异步编排的方式,别名: B 线程
        CompletableFuture<Void> bFuture = CompletableFuture.runAsync(() -> {
            //2、远程调用B服务,查某数据。
           //代码略...
        }, executor)
		
        //allOf().get(),阻塞等待,A,B异步任务均执行完毕
        CompletableFuture.allOf(aFuture,bFuture).get();

        return orderConfirmVo;
    }

2、 分析问题原因:

  • ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes() 内部使用了 ThreadLocalThreadLocal只能获取“当前线程”的上下文数据,即仅在当前线程内共享RequestAttributes数据。
  • 因此,在同步执行的情况下,A 和 B 远程调用均和主线程在同一个线程内执行,上下文数据共享。
  • 然而,在异步执行的情况下,需要的请求头数据在主线程中,而主线程、异步线程A、异步线程B之间,ThreadLocal是互相隔离的,所以导致“异步线程A”和“异步线程B”均获取不到 “主线程的请求上下文”。

Feign 远程调用丢失请求上下文问题_第4张图片

3、 解决方案:

知道了原因后,解决的办法就很简单了:

  • 将主线程获取的 ”请求上下文“,重新设置到 ”异步线程A“和”异步线程B“中,让”异步线程A“和”异步线程B“带这”主线程的请求上下文“,去远程调用Feign,即可。

示例代码如下:

	@Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        //主线程....
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        
         //获取"主线程"的请求上下文
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        
        //启用异步编排的方式,别名: A 线程
        CompletableFuture<Void> aFuture = CompletableFuture.runAsync(() -> {
             //将"主线程"的请求上下文,设置在当前“异步线程上下文”中。
            RequestContextHolder.setRequestAttributes(requestAttributes);
            
            //1、远程调用A服务 ,查某数据。
         	//代码略...
        }, executor);

        //启用异步编排的方式,别名: B 线程
        CompletableFuture<Void> bFuture = CompletableFuture.runAsync(() -> {
             //将"主线程"的请求上下文,设置在当前“异步线程上下文”中。
            RequestContextHolder.setRequestAttributes(requestAttributes);
            
            //2、远程调用B服务,查某数据。
           //代码略...
        }, executor)
		
        //allOf().get(),阻塞等待,A,B异步任务均执行完毕
        CompletableFuture.allOf(aFuture,bFuture).get();

        return orderConfirmVo;
    }

你可能感兴趣的:(SpringCloud,java,spring,boot,Feign)