大家好,我是飄渺。
在上一篇文章中,我們解決了網關層認證后向后端服務傳遞用戶信息的問題。今天我們來解決另外一個問題:如何在 OpenFeign 中傳遞 Token,并且保證多線程情況下也能適用。
這是DDD&微服務系列文章的第34篇,歡迎持續關注!
為了方便演示,首先定義一個接口,在接口中通過 Feign 調用其他服務:
@Operation(summary = "用戶測試接口") @GetMapping("/api/pd/customer/info") public String info() { String currentUser = UserContextHolder.getInstance().getCurrentUser(); log.info("feign調用方獲取當前登錄用戶:" + currentUser); //通過feign調用遠程服務 String info = experimentClient.info(); log.info("遠程獲取用戶:" + info); return currentUser; }
然后在遠程接口中通過上文定義的UserContextHolder對象獲取用戶信息:
@GetMapping("/api/pd/experiment/info") public String userInfo() { String currentUser = UserContextHolder.getInstance().getCurrentUser(); log.info("feign被調用方獲取userToken : {} ",currentUser); return currentUser == null ? "" : currentUser; }
圖片
通過調用結果可知,當使用OpenFeign調用遠程服務時,接口是無法獲取到用戶 ID 的。
在使用OpenFeign請求其他服務接口時,默認不攜帶header信息,這樣就導致無法攜帶登錄用戶信息。常規情況下,我們只需要在使用 OpenFeign 調用時先從 Header 獲取 Token 信息,放入新請求即可,在項目中可以定義一個OpenFeign的攔截器來實現此功能,代碼如下所示:
public class FeignRequestConfiguration { @Bean public RequestInterceptor requestInterceptor(){ return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) requestAttributes; // 當主線程的請求執行完畢后,Servlet容器會被銷毀當前的Servlet,因此在這里需要做判空 if (attributes != null) { HttpServletRequest request = attributes.getRequest(); // 獲取userId 并傳遞 userId String userId = request.getHeader(CommonConstant.X_CLIENT_TOKEN); if (StringUtils.hasText(userId)) { template.header(CommonConstant.X_CLIENT_TOKEN, userId); } } } }; } }
經過上述配置以后再次調用即可在 Feign 接口中也獲取到用戶ID,如下圖所示:
圖片
上面是單線程的情況,假如我們在當前線程中又開啟了子線程去進行 Feign 調用,那么是無法從 RequestContextHolder 獲取到 Header 的。測試代碼如下:
public String info() { String currentUser = UserContextHolder.getInstance().getCurrentUser(); log.info("feign調用方獲取當前登錄用戶:" + currentUser); CompletableFuture<String> infoFuture = CompletableFuture.supplyAsync(experimentClient::info,executor); String info = ""; try{ info = infoFuture.get(); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException(e); } log.info("遠程獲取用戶:" + info); return currentUser; }
在上述代碼中,通過 CompletableFuture 開啟異步線程去調用 experimentClient ,可以發現此時無法獲取到用戶信息,效果如下所示:
圖片
出現上述問題的原因是,RequestContextHolder.getRequestAttributes() 方法里面使用的一個 ThreadLocal,默認不是線程共享的,源碼如下:
public static RequestAttributes getRequestAttributes() { RequestAttributes attributes = requestAttributesHolder.get(); if (attributes == null) { attributes = inheritableRequestAttributesHolder.get(); } return attributes; }
所以主線程調用子線程時,無法獲取到主線程請求里面的 RequestAttributes。
原因已經清楚了,繼續觀察 RequestContextHolder.getRequestAttributes() 方法源碼,注意到如果當前線程拿不到 RequestAttributes ,它會從 inheritableRequestAttributesHolder 里面拿,再仔細觀察發現源碼設置 RequestAttributes 到 ThreadLocal 的時候有這樣一個重載方法。
/** * 給當前線程綁定屬性 * @param inheritable 是否要將屬性暴露給子線程 */public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) { ......}
這看起來符合我們的要求,只需要在主線程調用其他線程前將 RequestAttributes 對象設置為子線程共享,就能把 Header 等信息傳遞下去。
所以,在異步調用 Feign 接口時添加如下代碼即可:
RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(),true);CompletableFuture<String> infoFuture = CompletableFuture.supplyAsync(experimentClient::info,executor);......
再次執行發現,是可以獲取到 userId 的。
這里使用CompletableFuture異步調用時需要使用自定義線程池,而不能使用默認線程池ForkJoinPool,這是為什么呢?
雖然可以在異步調用時設置 RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true); 可以實現請求頭透傳,但是每次調用都需要加上這一句,實現上還略顯麻煩。
并且我們知道了獲取不到請求頭的原因是子線程無法獲取主線程的 header 屬性,那么我們只需要定義一個數據結構,使用 InheritableThreadLocal 在內存中保存一份 header 屬性即可。在上篇文章中通過網關進行 UserID 透傳時我們是使用 ThreadLocal 保存數據,現在只需要將其換成 InheritableThreadLocal,同時在 RequestInterceptor#apply() 方法中不再通過請求頭獲取而是直接從 InheritableThreadLocal 中獲取數據。
實現過程如下:
1、重命名并修改數據結構:
首先,將 UserContextHolder 重命名為 RequestHeaderHolder,同時使用 InheritableThreadLocal 替換 ThreadLocal,以便子線程也能獲取數據。
public class RequestHeaderHolder { private final ThreadLocal<Map<String,String>> REQUEST_HEADER_HOLDER; //使用InheritableThreadLocal,使得共享變量可被子線程繼承 private RequestHeaderHolder() { this.REQUEST_HEADER_HOLDER = new InheritableThreadLocal<>() { @Override protected Map<String, String> initialValue() { return new HashMap<>(); } }; } public String getCurrentUser(){ return this.REQUEST_HEADER_HOLDER.get().get(CommonConstant.X_CLIENT_TOKEN); } ......}
2、修改請求攔截器:
將請求攔截器 UserTokenInterceptor 重命名為 RequestHeaderInterceptor,并將請求頭放入 RequestHeaderHolder 中。
@Slf4jpublic class RequestHeaderInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) throws Exception { Enumeration<String> headerNames = request.getHeaderNames(); RequestHeaderHolder requestHeaderHolder = RequestHeaderHolder.getInstance(); //重新設置請求頭 while (headerNames.hasMoreElements()){ String key = headerNames.nextElement(); requestHeaderHolder.set(key,request.getHeader(key)); } return true; } ......}
3、修改 Feign 配置類:在 FeignRequestConfiguration 中不再從 RequestContextHolder 獲取數據,而是從 RequestHeaderHolder 獲取數據。
@Slf4jpublic class FeignRequestConfiguration { @Bean public RequestInterceptor requestInterceptor(){ return template -> { Map<String, String> headerMap = RequestHeaderHolder.getInstance().get(); if(headerMap != null){ headerMap.forEach((key, value) -> { template.header(key, value); }); } }; }}
通過上面的改造,不管是同步調用還是子線程異步調用都可以直接通過RequestHeaderHolder.getInstance().getCurrentUser();獲取用戶信息,并且調用方無須做任何改動。
本文鏈接:http://www.www897cc.com/showinfo-26-79599-0.htmlSpringCloud微服務中Feign如何傳遞用戶Token,并保證多線程環境也可適用?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 你的Css選擇器可視化備忘錄