业务场景

通常微服务对于用户认证信息解析有两种方案

  • gateway 就解析用户的 token 然后路由的时候把 userId 等相关信息添加到 header 中传递下去。
  • gateway 直接把 token 传递下去,每个子微服务自己在过滤器解析 token

现在有一个从 A 服务调用 B 服务接口的内部调用业务场景,无论是哪种方案我们都需要把 header 从 A 服务传递到 B 服务。

RequestInterceptor

OpenFeign 给我们提供了一个请求拦截器 RequestInterceptor ,我们可以实现这个接口重写 apply 方法将当前请求的 header 添加到请求中去,传递给下游服务,RequestContextHolder 可以获得当前线程绑定的 Request 对象

/** Feign 调用的时候传token到下游 */
public class FeignRequestInterceptor implements RequestInterceptor {
  @Override
  public void apply(RequestTemplate template) {
    // 从header获取X-token
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes attr = (ServletRequestAttributes) requestAttributes;
    HttpServletRequest request = attr.getRequest();
    String token = request.getHeader("x-auth-token");//网关传过来的 token
    if (StringUtils.hasText(token)) {
      template.header("X-AUTH-TOKEN", token);
    }
  }
}

然后在 @FeignClient 中使用

@FeignClient(
    ...
    configuration = {FeignClientDecoderConfiguration.class, FeignRequestInterceptor.class})
public interface AuthCenterClient {

多线程环境下传递 header(一)

上面是单线程的情况,假如我们在当前线程中又开启了子线程去进行 Feign 调用,那么是无法从 RequestContextHolder 获取到 header 的,原因很简单,看下 RequestContextHolder 源码就知道了,它里面是一个 ThreadLocal ,线程都变了,那肯定获取不到主线程请求里面的 requestAttribute 了。

原因已经清楚了,现在想办法去解决它。观察 RequestContextHolder.getRequestAttributes() 方法源码

public static RequestAttributes getRequestAttributes() {
   RequestAttributes attributes = requestAttributesHolder.get();
   if (attributes == null) {
      attributes = inheritableRequestAttributesHolder.get();
   }
   return attributes;
}

注意到如果当前线程拿不到 RequestAttributes ,他会从 inheritableRequestAttributesHolder 里面拿,再仔细观察发现源码设置 RequestAttributesThreadLocal 的时候有这样一个重载方法

/**
 * 给当前线程绑定属性
 * @param inheritable 是否要将属性暴露给子线程
 */
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
   //......
}

这特喵的完美符合我们的需求,现在我们的问题就是子线程没有拿到主线程的 RequestContextHolder 里面的属性。在业务代码中:

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
log.info("主线程任务....");
new Thread(() -> {
    log.info("子线程任务开始...");
    UserResponse response = client.getById(3L);
}).start();

开发环境测试之后发现子线程已经能够从 RequestContextHolder 拿到主线程的请求对象了。

分析 inheritableRequestAttributesHolder 原理

观察源码我们可以看到这个属性的类型是 NamedInheritableThreadLocal 它继承了 InheritableThreadLocal 。还记得去年我第一次遇到开启多线程跨服务请求的时候始终不能理解为什么这玩意能把当前线程绑定的对象暴露给子线程。前几天 debug 了一下 InheritableThreadLocal.set() 方法恍然大悟。

其实这个东西对 Thread、ThreadLocal 有了解就会知道,在 Thread 的构造方法里面有这样一段代码

//...
Thread parent = currentThread(); //创建子线程的时候先拿父线程
//...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals;
//...

其实我们创建子线程的时候会先拿父线程,判断父线程里面的 inheritableThreadLocals 是不是有值,由于上面 RequestContextHolder.setRequestAttributes(xxx,true) 设置了 true ,所以父线程的 inheritableThreadLocals 是有 requestAttributes 的。这样创建子线程后,子线程的 inheritableThreadLocals 也有值了。所以后面我们在子线程中获取 requestAttributes 是能获取到的。

这样真的解决问题了吗?从非 web 层面来看,的确是解决了这个问题,但是在我们的 web 场景中并非如此。经过反复的测试,我们会发现子线程并不是每次都能获取到 header ,进而我们发现了这与父子线程的结束顺序有关,如果父线程早与子线程结束,那么子线程就获取不到 header ,反之子线程能获取到 header

分析 inheritableRequestAttributesHolder 失效原因

其实标题并不严谨,因为子线程获取不到请求的 header 并不是因为 inheritableRequestAttributesHolder 失效。这个原因当初我也很奇怪,于是我从网上看到一篇文章,它是这么写的。

在源码中ThreadLocal对象保存的是RequestAttributes attributes;这个是保存的对象的引用一旦父线程销毁了,那RequestAttributes也会被销毁,那RequestAttributes的引用地址的值就为null**;**虽然子线程也有RequestAttributes的引用,但是引用的值为null了。

真的是这样吗??我怎么看怎么感觉不对......于是我自己验证了下

@GetMapping("/test")
public void test(HttpServletRequest request) {
    RequestAttributes attr = RequestContextHolder.getRequestAttributes();
    log.info("父线程:RequestAttributes:{}", attr);
    RequestContextHolder.setRequestAttributes(attr, true);
    log.info("父线程:SpringMVC:request:{}",request);
    log.info("父线程:x-auth-token:{}",request.getHeader("x-auth-token"));
    ServletRequestAttributes attr1 = (ServletRequestAttributes) attr;
    HttpServletRequest request1 = attr1.getRequest();
    log.info("父线程:request:{}",request1);
    new Thread(
            () -> {
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                RequestAttributes childAttr = RequestContextHolder.getRequestAttributes();
                log.info("子线程:RequestAttributes:{}",childAttr);
                ServletRequestAttributes childServletRequestAttr = (ServletRequestAttributes) childAttr;
                HttpServletRequest childRequest = childServletRequestAttr.getRequest();
                log.info("子线程:childRequest:{}",childRequest);
                String childToken = childRequest.getHeader("x-auth-token");
                log.info("子线程:x-auth-token:{}",childToken);
            }).start();
}

观察日志

父线程:RequestAttributes:org.apache.catalina.connector.RequestFacade@ea25271
父线程:SpringMVC:request:org.apache.catalina.connector.RequestFacade@ea25271
父线程:x-auth-token:null
父线程:request:org.apache.catalina.connector.RequestFacade@ea25271

子线程:RequestAttributes:org.apache.catalina.connector.RequestFacade@ea25271
子线程:childRequest:org.apache.catalina.connector.RequestFacade@ea25271
子线程:x-auth-token:{}:null

很明显子线程拿到了 RequestAttitutes 对象,而且和父线程是同一个,这就推翻了上面的说法,并不是引用变为 null 了导致的。那么到底是什么原因导致父线程结束后,子线程就拿不到 request 对象里面的 header 属性了呢?

我们可以猜测一下,既然父线程和子线程拿到的 request 对象是同一个,并且在子线程代码中 request 对象还不是 null,但是属性没了,那应该是请求结束之后某个地方对 request 对象进行了属性移除。我们跟随 RequestFacade 类去寻找真理,寻找寻找再寻找......终于我发现了真相在 org.apache.coyote.Request

Tomcat 内部,请求结束后会对 request 对象重置,把 header 等属性移除,是因为这样如果父线程提前结束,我们在子线程中才无法获取 request 对象的 header

或许你可以再思考一下 Tomcat 为什么要这么做?

多线程环境下传递 header(二)

既然 RequestContextHolder.setRequestAttributes(attr, true); 也不能完全实现子线程能够获取父线程的 header ,那么我们如何解决呢?

控制主线程在子线程结束后再结束

这是最简单的方法,我把父线程挂起来,等子线程任务都执行完了,再结束父线程,这样就不会出现子线程获取不到 header 的情况了。最简单的,我们可以用 ExecutorCompletionService 实现。

重新保存 request 的 header

上面我们已经知道了获取不到 header 是因为 request 对象的 header 属性被移除了,那么我们只需要自己定义一个数据结构 ThreadLocal 重新在内存中保存一份 header 属性即可。我们可以定义一个请求拦截器,在拦截器中获取 headers 放到自定义的结构中。

定义结构

public class RequestHeaderHolder {
    private static final ThreadLocal<Map<String,String>> REQUEST_HEADER_HOLDER = new InheritableThreadLocal<>(){
        @Override
        protected Map<String, String> initialValue() {
            return new HashMap<>();
        }
    };
    //...省略部分方法
}

拦截器

public class RequestHeaderInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Enumeration<String> headerNames = request.getHeaderNames();

        while (headerNames.hasMoreElements()){
            String s = headerNames.nextElement();
            RequestHeaderHolder.set(s,request.getHeader(s));
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        RequestHeaderHolder.remove(); //注意一定要remove
    }
}

然后将这个拦截器添加到 InterceptorRegistry 即可。这样我们在子线程中就可以通过 RequestHeaderHolder 获取请求到 header

结语

本篇文章简单介绍 OpenFeign 调用传递 header ,以及多线程环境下可能会出现的问题。其中涉及到 ThreadLocal 的相关知识,如果有同学对 ThreadLocal、InheritableThreadLocal 不清楚的可以留言,后面出一篇 ThreadLocal 的文章。

到此这篇关于SpringCloud OpenFeign 服务调用传递 token的场景分析的文章就介绍到这了,更多相关SpringCloud OpenFeign传递 token内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

SpringCloud OpenFeign 服务调用传递 token的场景分析的更多相关文章

  1. AngularJS下$http服务Post方法传递json参数的实例

    下面小编就为大家分享一篇AngularJS下$http服务Post方法传递json参数的实例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  2. SpringCloud超详细讲解微服务网关Zuul基础

    这篇文章主要介绍了SpringCloud Zuul微服务网关,负载均衡,熔断和限流,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  3. SpringCloud gateway+zookeeper实现网关路由的详细搭建

    这篇文章主要介绍了SpringCloud gateway+zookeeper实现网关路由,本文通过图文实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. PHP页面间传递值和保持值的方法

    这篇文章主要介绍了PHP页面间传递值和保持值的方法,传递值主要通过get和post提交,通过session和cookie保持数据,本文介绍的非常详细,具有参考借鉴价值,需要的朋友可以参考下

  5. 详解OpenFeign服务调用(微服务)

    OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等,这篇文章主要介绍了OpenFeign服务调用的相关知识,需要的朋友可以参考下

  6. Spring Cloud OpenFeign实例介绍使用方法

    Spring Cloud OpenFeign 对 Feign 进行了二次封装,使得在 Spring Cloud 中使用 Feign 的时候,可以做到使用 HTTP 请求访问远程服务,就像调用本地方法一样的,开发者完全感知不到这是在调用远程访问,更感知不到在访问 HTTP 请求

  7. 通过实例了解js函数中参数的传递

    这篇文章主要介绍了通过实例了解js函数中参数的传递,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,,需要的朋友可以参考下

  8. SpringCloud OpenFeign 服务调用传递 token的场景分析

    这篇文章主要介绍了SpringCloud OpenFeign 服务调用传递 token的场景分析,本篇文章简单介绍 OpenFeign 调用传递 header ,以及多线程环境下可能会出现的问题,其中涉及到 ThreadLocal 的相关知识,需要的朋友可以参考下

  9. OpenFeign实现远程调用

    这篇文章主要为大家详细介绍了OpenFeign实现远程调用,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android跨进程传递大数据的方法实现

    这篇文章主要介绍了Android跨进程传递大数据的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

随机推荐

  1. Java利用POI实现导入导出Excel表格

    这篇文章主要为大家详细介绍了Java利用POI实现导入导出Excel表格,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  2. Mybatis分页插件PageHelper手写实现示例

    这篇文章主要为大家介绍了Mybatis分页插件PageHelper手写实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  3. (jsp/html)网页上嵌入播放器(常用播放器代码整理)

    网页上嵌入播放器,只要在HTML上添加以上代码就OK了,下面整理了一些常用的播放器代码,总有一款适合你,感兴趣的朋友可以参考下哈,希望对你有所帮助

  4. Java 阻塞队列BlockingQueue详解

    本文详细介绍了BlockingQueue家庭中的所有成员,包括他们各自的功能以及常见使用场景,通过实例代码介绍了Java 阻塞队列BlockingQueue的相关知识,需要的朋友可以参考下

  5. Java异常Exception详细讲解

    异常就是不正常,比如当我们身体出现了异常我们会根据身体情况选择喝开水、吃药、看病、等 异常处理方法。 java异常处理机制是我们java语言使用异常处理机制为程序提供了错误处理的能力,程序出现的错误,程序可以安全的退出,以保证程序正常的运行等

  6. Java Bean 作用域及它的几种类型介绍

    这篇文章主要介绍了Java Bean作用域及它的几种类型介绍,Spring框架作为一个管理Bean的IoC容器,那么Bean自然是Spring中的重要资源了,那Bean的作用域又是什么,接下来我们一起进入文章详细学习吧

  7. 面试突击之跨域问题的解决方案详解

    跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站窃取数据。那怎么解决这个问题呢?接下来我们一起来看

  8. Mybatis-Plus接口BaseMapper与Services使用详解

    这篇文章主要为大家介绍了Mybatis-Plus接口BaseMapper与Services使用详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

  9. mybatis-plus雪花算法增强idworker的实现

    今天聊聊在mybatis-plus中引入分布式ID生成框架idworker,进一步增强实现生成分布式唯一ID,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Spring JdbcTemplate执行数据库操作详解

    JdbcTemplate是Spring框架自带的对JDBC操作的封装,目的是提供统一的模板方法使对数据库的操作更加方便、友好,效率也不错,这篇文章主要介绍了Spring JdbcTemplate执行数据库操作,需要的朋友可以参考下

返回
顶部