一、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
拦截器的实现类。
方案一:
- 可以使用默认提供的拦截器,拦截器实现如下图。 没有添加 Cookie的拦截器实现,自己实现一个即可。
方案二:
- 自定义 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>,同一个线程可以共享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<RequestInterceptor> ,默认是空的
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
//相当于 headers请求头没处理,是空的,这就是Feign调用请求头信息丢失的原因。解决办法需要自己添加拦截器。
return target.apply(template);
}
}
【重点】构造目标请求对象
targetRequest()
方法,会通过拦截器处理请求上下文。如果没有自定义拦截器,则默认的就是不处理请求头。
二、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()
内部使用了ThreadLocal<RequestAttributes>
,ThreadLocal
只能获取“当前线程”的上下文数据,即仅在当前线程内共享RequestAttributes
数据。- 因此,在同步执行的情况下,A 和 B 远程调用均和主线程在同一个线程内执行,上下文数据共享。
- 然而,在异步执行的情况下,需要的请求头数据在主线程中,而主线程、异步线程A、异步线程B之间,
ThreadLocal
是互相隔离的,所以导致“异步线程A”和“异步线程B”均获取不到 “主线程的请求上下文”。
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;
}