业务场景
通常微服务对于用户认证信息解析有两种方案
- 在
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
里面拿,再仔细观察发现源码设置 RequestAttributes
到 ThreadLocal
的时候有这样一个重载方法
/**
* 给当前线程绑定属性
* @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
的文章。
作者:暮色妖娆丶
链接:https://juejin.cn/post/7123096319371001870
来源:稀土掘金
网友评论