欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

SpringCloud?OpenFeign?服務(wù)調(diào)用傳遞?token的場景分析

 更新時間:2022年07月26日 11:50:35   作者:暮色妖嬈丶  
這篇文章主要介紹了SpringCloud?OpenFeign?服務(wù)調(diào)用傳遞?token的場景分析,本篇文章簡單介紹?OpenFeign?調(diào)用傳遞?header?,以及多線程環(huán)境下可能會出現(xiàn)的問題,其中涉及到?ThreadLocal?的相關(guān)知識,需要的朋友可以參考下

業(yè)務(wù)場景

通常微服務(wù)對于用戶認(rèn)證信息解析有兩種方案

  • gateway 就解析用戶的 token 然后路由的時候把 userId 等相關(guān)信息添加到 header 中傳遞下去。
  • gateway 直接把 token 傳遞下去,每個子微服務(wù)自己在過濾器解析 token

現(xiàn)在有一個從 A 服務(wù)調(diào)用 B 服務(wù)接口的內(nèi)部調(diào)用業(yè)務(wù)場景,無論是哪種方案我們都需要把 header 從 A 服務(wù)傳遞到 B 服務(wù)。

RequestInterceptor

OpenFeign 給我們提供了一個請求攔截器 RequestInterceptor ,我們可以實現(xiàn)這個接口重寫 apply 方法將當(dāng)前請求的 header 添加到請求中去,傳遞給下游服務(wù),RequestContextHolder 可以獲得當(dāng)前線程綁定的 Request 對象

/** Feign 調(diào)用的時候傳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");//網(wǎng)關(guān)傳過來的 token
    if (StringUtils.hasText(token)) {
      template.header("X-AUTH-TOKEN", token);
    }
  }
}

然后在 @FeignClient 中使用

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

多線程環(huán)境下傳遞 header(一)

上面是單線程的情況,假如我們在當(dāng)前線程中又開啟了子線程去進(jìn)行 Feign 調(diào)用,那么是無法從 RequestContextHolder 獲取到 header 的,原因很簡單,看下 RequestContextHolder 源碼就知道了,它里面是一個 ThreadLocal ,線程都變了,那肯定獲取不到主線程請求里面的 requestAttribute 了。

原因已經(jīng)清楚了,現(xiàn)在想辦法去解決它。觀察 RequestContextHolder.getRequestAttributes() 方法源碼

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

注意到如果當(dāng)前線程拿不到 RequestAttributes ,他會從 inheritableRequestAttributesHolder 里面拿,再仔細(xì)觀察發(fā)現(xiàn)源碼設(shè)置 RequestAttributesThreadLocal 的時候有這樣一個重載方法

/**
 * 給當(dāng)前線程綁定屬性
 * @param inheritable 是否要將屬性暴露給子線程
 */
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
   //......
}

這特喵的完美符合我們的需求,現(xiàn)在我們的問題就是子線程沒有拿到主線程的 RequestContextHolder 里面的屬性。在業(yè)務(wù)代碼中:

RequestContextHolder.setRequestAttributes(RequestContextHolder.getRequestAttributes(), true);
log.info("主線程任務(wù)....");
new Thread(() -> {
    log.info("子線程任務(wù)開始...");
    UserResponse response = client.getById(3L);
}).start();

開發(fā)環(huán)境測試之后發(fā)現(xiàn)子線程已經(jīng)能夠從 RequestContextHolder 拿到主線程的請求對象了。

分析 inheritableRequestAttributesHolder 原理

觀察源碼我們可以看到這個屬性的類型是 NamedInheritableThreadLocal 它繼承了 InheritableThreadLocal 。還記得去年我第一次遇到開啟多線程跨服務(wù)請求的時候始終不能理解為什么這玩意能把當(dāng)前線程綁定的對象暴露給子線程。前幾天 debug 了一下 InheritableThreadLocal.set() 方法恍然大悟。

其實這個東西對 Thread、ThreadLocal 有了解就會知道,在 Thread 的構(gòu)造方法里面有這樣一段代碼

//...
Thread parent = currentThread(); //創(chuàng)建子線程的時候先拿父線程
//...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals;
//...

其實我們創(chuàng)建子線程的時候會先拿父線程,判斷父線程里面的 inheritableThreadLocals 是不是有值,由于上面 RequestContextHolder.setRequestAttributes(xxx,true) 設(shè)置了 true ,所以父線程的 inheritableThreadLocals 是有 requestAttributes 的。這樣創(chuàng)建子線程后,子線程的 inheritableThreadLocals 也有值了。所以后面我們在子線程中獲取 requestAttributes 是能獲取到的。

這樣真的解決問題了嗎?從非 web 層面來看,的確是解決了這個問題,但是在我們的 web 場景中并非如此。經(jīng)過反復(fù)的測試,我們會發(fā)現(xiàn)子線程并不是每次都能獲取到 header ,進(jìn)而我們發(fā)現(xiàn)了這與父子線程的結(jié)束順序有關(guān),如果父線程早與子線程結(jié)束,那么子線程就獲取不到 header ,反之子線程能獲取到 header。

分析 inheritableRequestAttributesHolder 失效原因

其實標(biāo)題并不嚴(yán)謹(jǐn),因為子線程獲取不到請求的 header 并不是因為 inheritableRequestAttributesHolder 失效。這個原因當(dāng)初我也很奇怪,于是我從網(wǎng)上看到一篇文章,它是這么寫的。

在源碼中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 對象,而且和父線程是同一個,這就推翻了上面的說法,并不是引用變?yōu)?null 了導(dǎo)致的。那么到底是什么原因?qū)е赂妇€程結(jié)束后,子線程就拿不到 request 對象里面的 header 屬性了呢?

我們可以猜測一下,既然父線程和子線程拿到的 request 對象是同一個,并且在子線程代碼中 request 對象還不是 null,但是屬性沒了,那應(yīng)該是請求結(jié)束之后某個地方對 request 對象進(jìn)行了屬性移除。我們跟隨 RequestFacade 類去尋找真理,尋找尋找再尋找......終于我發(fā)現(xiàn)了真相在 org.apache.coyote.Request

Tomcat 內(nèi)部,請求結(jié)束后會對 request 對象重置,把 header 等屬性移除,是因為這樣如果父線程提前結(jié)束,我們在子線程中才無法獲取 request 對象的 header

或許你可以再思考一下 Tomcat 為什么要這么做?

多線程環(huán)境下傳遞 header(二)

既然 RequestContextHolder.setRequestAttributes(attr, true); 也不能完全實現(xiàn)子線程能夠獲取父線程的 header ,那么我們?nèi)绾谓鉀Q呢?

控制主線程在子線程結(jié)束后再結(jié)束

這是最簡單的方法,我把父線程掛起來,等子線程任務(wù)都執(zhí)行完了,再結(jié)束父線程,這樣就不會出現(xiàn)子線程獲取不到 header 的情況了。最簡單的,我們可以用 ExecutorCompletionService 實現(xiàn)。

重新保存 request 的 header

上面我們已經(jīng)知道了獲取不到 header 是因為 request 對象的 header 屬性被移除了,那么我們只需要自己定義一個數(shù)據(jù)結(jié)構(gòu) ThreadLocal 重新在內(nèi)存中保存一份 header 屬性即可。我們可以定義一個請求攔截器,在攔截器中獲取 headers 放到自定義的結(jié)構(gòu)中。

定義結(jié)構(gòu)

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 。

結(jié)語

本篇文章簡單介紹 OpenFeign 調(diào)用傳遞 header ,以及多線程環(huán)境下可能會出現(xiàn)的問題。其中涉及到 ThreadLocal 的相關(guān)知識,如果有同學(xué)對 ThreadLocal、InheritableThreadLocal 不清楚的可以留言,后面出一篇 ThreadLocal 的文章。

到此這篇關(guān)于SpringCloud OpenFeign 服務(wù)調(diào)用傳遞 token的場景分析的文章就介紹到這了,更多相關(guān)SpringCloud OpenFeign傳遞 token內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 淺談Java設(shè)計模式之七大設(shè)計原則

    淺談Java設(shè)計模式之七大設(shè)計原則

    在此之前,我已經(jīng)寫過很多篇關(guān)于設(shè)計模式的文章.但都比較草草的理解和簡單的實現(xiàn),并未深入理解.為了更加深入感受Java設(shè)計的魅力,編程的藝術(shù),今天進(jìn)行了七大設(shè)計原則的學(xué)習(xí)理解,后續(xù)進(jìn)行23種設(shè)計模式的深入學(xué)習(xí)探究,需要的朋友可以參考下
    2021-05-05
  • 使用CI/CD工具Github Action發(fā)布jar到Maven中央倉庫的詳細(xì)介紹

    使用CI/CD工具Github Action發(fā)布jar到Maven中央倉庫的詳細(xì)介紹

    今天通過對Github Action的簡單使用來介紹了CI/CD的作用,這個技術(shù)體系是項目集成交付的趨勢,也是面試中的一個亮點技能。 而且這種方式可以實現(xiàn)“一次配置,隨時隨地集成部署”,感興趣的朋友一起看看吧
    2021-07-07
  • java中對象轉(zhuǎn)json字符串的幾種常用方式舉例

    java中對象轉(zhuǎn)json字符串的幾種常用方式舉例

    這篇文章主要給大家介紹了關(guān)于java中對象轉(zhuǎn)json字符串的幾種常用方式,在Java中可以使用許多庫將對象轉(zhuǎn)換為JSON字符串,其中最常用的是Jackson和Gson,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2023-10-10
  • 全面詳解Spring?Bean生命周期教程示例

    全面詳解Spring?Bean生命周期教程示例

    這篇文章主要為大家介紹了Spring?Bean生命周期的全面詳解教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-04-04
  • Java中controller層如何接收帶參數(shù)的查詢

    Java中controller層如何接收帶參數(shù)的查詢

    本文主要介紹了Java中controller層如何接收帶參數(shù)的查詢,在控制器層接收帶參數(shù)的查詢可以通過多種方式實現(xiàn),下面就詳細(xì)的介紹一下,感興趣的可以了解一下
    2023-08-08
  • 如何獲取所有spring管理的bean

    如何獲取所有spring管理的bean

    這篇文章主要介紹了如何獲取所有spring管理的bean,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2021-09-09
  • Java解決青蛙跳臺階問題流程

    Java解決青蛙跳臺階問題流程

    所謂的青蛙跳臺階問題,就是指一只青蛙一次可以跳上1級臺階,也可以跳上2級。求該青蛙跳上一個n級的臺階總共有多少種跳法。本文將用Java解決這一問題,需要的可以參考一下
    2022-03-03
  • JAVA內(nèi)存模型和Happens-Before規(guī)則知識點講解

    JAVA內(nèi)存模型和Happens-Before規(guī)則知識點講解

    在本篇文章里小編給大家整理的是一篇關(guān)于JAVA內(nèi)存模型和Happens-Before規(guī)則知識點內(nèi)容,有需要的朋友們跟著學(xué)習(xí)下。
    2020-11-11
  • Spring實現(xiàn)擁有者權(quán)限驗證的方法示例

    Spring實現(xiàn)擁有者權(quán)限驗證的方法示例

    這篇文章主要介紹了Spring實現(xiàn)擁有者權(quán)限驗證的方法示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-03-03
  • 淺談Spring中如何使用設(shè)計模式

    淺談Spring中如何使用設(shè)計模式

    這篇文章主要介紹了淺談Spring中如何使用設(shè)計模式,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-05-05

最新評論