你好,我是小航,一个正在变秃、变强的文艺倾年。
本文讲解Feign远程调用和异步调用请求头丢失问题,欢迎大家多多关注!
每天进步一点点,一起卷起来叭!
目录
- 前言
- Feign远程调用丢失请求头
- Feign异步情况丢失上下文问题
最近在梳理以前做过的项目:遇到了俩问题,第一个问题是,在微服务项目中,我们做了单点登录
,在项目使用feign远程调用
另一个模块的远程服务时,发现提示无权限调用。第二个问题是异步调用时,老请求线程不共享问题,导致业务获取不到老请求报空指针异常。
为什么会丢失请求头?
//1.在远程调用的方法上打个断点
List<MemberAddressVo> address = memberFeignService.getAddress(memberRespVo.getId());
//2.进入方法内部 ReflectiveFeign.class
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判断调用是不是equal方法
if (!"equals".equals(method.getName())) {
//判断是不是调用hashCode
if ("hashCode".equals(method.getName())) {
return this.hashCode();
} else {
//判断是不是调用toString 都不是就执行 ((MethodHandler)this.dispatch.get(method)).invoke(args);
return "toString".equals(method.getName()) ? this.toString() : ((MethodHandler)this.dispatch.get(method)).invoke(args);
}
} else {
try {
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
return this.equals(otherHandler);
} catch (IllegalArgumentException var5) {
return false;
}
}
}
//3. ((MethodHandler)this.dispatch.get(method)).invoke(args);
//点击进入invoke 方法 SynchronousMethodHandler.class
public Object invoke(Object[] argv) throws Throwable {
//就是在这 构建了一个新的RequestTemplate ,而浏览器带给我们的请求头都会丢失
RequestTemplate template = this.buildTemplateFromArgs.create(argv);
Retryer retryer = this.retryer.clone();
while(true) {
try {
//在这即将执行该方法
return this.executeAndDecode(template);
} catch (RetryableException var8) {
RetryableException e = var8;
try {
retryer.continueOrPropagate(e);
} catch (RetryableException var7) {
Throwable cause = var7.getCause();
if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
throw cause;
}
throw var7;
}
if (this.logLevel != Level.NONE) {
this.logger.logRetry(this.metadata.configKey(), this.logLevel);
}
}
}
}
至此,我们找到了feign远程调用请求头丢失的原因:
我们继续深入executeAndDecode方法
查看原因:
Object executeAndDecode(RequestTemplate template) throws Throwable {
//这里 它会对我们的请求进行一些包装
Request request = this.targetRequest(template);
if (this.logLevel != Level.NONE) {
this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
}
long start = System.nanoTime();
Response response;
try {
response = this.client.execute(request, this.options);
} catch (IOException var15) {
if (this.logLevel != Level.NONE) {
this.logger.logIOException(this.metadata.configKey(), this.logLevel, var15, this.elapsedTime(start));
}
throw FeignException.errorExecuting(request, var15);
}
//下面我们查看一下targetRequest方法
Request targetRequest(RequestTemplate template) {
//拿到对应的所有请求拦截器的迭代器
Iterator var2 = this.requestInterceptors.iterator();
//遍历所有的请求拦截器
while(var2.hasNext()) {
RequestInterceptor interceptor = (RequestInterceptor)var2.next();
//这里是每个请求拦截器 依次对该方法进行包装
interceptor.apply(template);
}
return this.target.apply(template);
}
//我们发现它是一个接口 所以可以重写一下这个方法 对我们的请求做一些包装 借鉴一下别的实现方法
public interface RequestInterceptor {
void apply(RequestTemplate var1);
}
public class BasicAuthRequestInterceptor implements RequestInterceptor {
public void apply(RequestTemplate template) {
template.header("Authorization", new String[]{this.headerValue});
}
}
找到原因所在就好解决问题了,于是我写了一个feign拦截器
,这里面注入了一个RequestInterceptor的对象,它是一个接口,我重写了它的apply方法,在里面拿到老请求中的请求头信息
,放到这个新的请求模板里,我这里更新的是cookie
。
@Configuration
public class GuliFeignConfig {
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
RequestInterceptor requestInterceptor = new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、使用RequestContextHolder拿到刚进来的请求数据
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
//老请求
HttpServletRequest request = requestAttributes.getRequest();
if (request != null) {
//2、同步请求头的数据(主要是cookie)
//把老请求的cookie值放到新请求上来,进行一个同步
String cookie = request.getHeader("Cookie");
template.header("Cookie", cookie);
}
}
}
};
return requestInterceptor;
}
}
至此问题完美解决!
最后我们小结一下:
在分布式项目中,发送请求大致就两种,一种是浏览器访问
,第二种是服务与服务之间通过OpenFeign远程调用
。浏览器发送请求时,它会带上请求头的信息的,所以不会导致cookie丢失,这样用户真实登录的情况下不会判断未登录的异常情况。深入源码发现,Feign会重新创建一个request,这个请求是没有任何请求头
的,这个请求模板会遍历请求拦截器的apply方法
来丰富这个请求模板。所以我们可以写一个feign拦截器
,里面注入一个RequestInterceptor的对象,重写它的apply方法,在里面拿到老请求中的请求头信息
,放到这个新的请求模板里。
在实际项目中我们很可能需要异步调用多个远程服务
,这个时候我们会发现 feign 请求头丢失的问题又出现了
//1.问题主要出在 RequestContextHolder.getRequestAttributes();上,点进这个方法 看下源码
@Nullable
public static RequestAttributes getRequestAttributes() {
//它是从requestAttributesHolder这里面取出来的
RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
if (attributes == null) {
attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
}
return attributes;
}
//2.接着追 我们发现requestAttributesHolder是一个NamedThreadLocal对象
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
//3.我们发现NamedThreadLocal继承自ThreadLocal
//而ThreadLocal是一个线程局部变量,在不同线程之间是独立的所以我们获取不到原先主线程的请求属性,即给请求头添加cookie失败
public class NamedThreadLocal<T> extends ThreadLocal<T> {
private final String name;
public NamedThreadLocal(String name) {
Assert.hasText(name, "Name must not be empty");
this.name = name;
}
public String toString() {
return this.name;
}
}
解决方案:(这里只提供了一种 变量复制)
// 1.获取之前的请求头数据
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//2.每一个线程都共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
//远程查询
....
}, executor);