• 【微服务】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);
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    至此,我们找到了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});
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    找到原因所在就好解决问题了,于是我写了一个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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    至此问题完美解决!

    最后我们小结一下:

    在分布式项目中,发送请求大致就两种,一种是浏览器访问,第二种是服务与服务之间通过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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    解决方案:(这里只提供了一种 变量复制)

      // 1.获取之前的请求头数据
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    
    CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
                //2.每一个线程都共享之前的请求数据
                RequestContextHolder.setRequestAttributes(requestAttributes);
                //远程查询
                ....
    }, executor);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    示例代码:
    在这里插入图片描述


    在这里插入图片描述

  • 相关阅读:
    更易用的OceanBase|生态工具征文大赛正式开启!
    素问·八正神明论原文
    ES 批量删除数据
    记录工作过程中一次业务优化
    [LiteratureReview]A Collaborative Visual SLAM Framework for Service Robots
    CSAPP实验记录(2)--------- Bomb
    用Cmake快速生成vs工程
    Redux学习与使用
    水果店怎么样开才能赚钱,怎样开水果店赚钱
    重学设计模式之 装饰者模式
  • 原文地址:https://blog.csdn.net/m0_51517236/article/details/127539379