業務場景
通常微服務對于用戶認證信息解析有兩種方案
- 在 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 。
原文
https://juejin.cn/post/7123096319371001870