0
点赞
收藏
分享

微信扫一扫

【微服务】Feign远程调用和异步调用请求头丢失问题


😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本文讲解Feign远程调用和异步调用请求头丢失问题,欢迎大家多多关注!
🔔每天进步一点点,一起卷起来叭!


目录

  • ​​前言​​
  • ​​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远程调用请求头丢失的原因:

【微服务】Feign远程调用和异步调用请求头丢失问题_jvm

我们继续深入​​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异步情况丢失上下文问题

在实际项目中我们很可能需要​​异步调用多个远程服务​​,这个时候我们会发现 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);

示例代码:

【微服务】Feign远程调用和异步调用请求头丢失问题_java_02

【微服务】Feign远程调用和异步调用请求头丢失问题_微服务_03


举报

相关推荐

0 条评论