【微服务】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远程调用和异步调用请求头丢失问题_第1张图片

我们继续深入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远程调用和异步调用请求头丢失问题_第2张图片


你可能感兴趣的:(微服务,微服务,java,jvm)