1. 程式人生 > 實用技巧 >feign遠端呼叫丟失請求頭原始碼分析與解決

feign遠端呼叫丟失請求頭原始碼分析與解決

前言

我們在寫服務端專案的時候,總會限制對某些資源的訪問,最常見的就是要求使用者先登入才能訪問資源,當用戶登入後就會將此次會話資訊儲存進session,同時返回給瀏覽器指定的cookie鍵值,下次瀏覽器再次訪問,請求頭中就會攜帶這個cookie,我們也以次來識別使用者的登入狀態,做出正確響應。

問題

有時候,我們先行登入,然後訪問服務A的某個方法,請求頭中攜帶cookie,標識我們已經登入。但若是我們訪問的目標方法在執行過程中使用feign進行原程呼叫服務B(假設不存在跨域),而服務B也要先判斷登入狀態,我們可能發現服務B會呼叫失敗,或者說拿不到資料,理由是服務B認為我們並未登入。而這時,如果我們直接從瀏覽器訪問服務B的這個方法卻能得到一個成功的響應。

也就是說:

瀏覽器--->服務A成功; 服務A-->服務B失敗; 瀏覽器-->服務B失敗
結合上面所說,服務AB都會先判斷使用者登入狀態,瀏覽器直接訪問AB時都會帶上登入成功後儲存的cookie,而服務A通過Feign遠端呼叫B,卻被認為未登入,顯然,這部分請求頭資料丟失。

feign原始碼分析

我們來看下feign遠端呼叫是如何執行的,我們在feign遠端呼叫之處打上斷點

  1. step into進入方法執行,會發現是一個代理物件的invoke方法在執行,首先判斷是方法名,
  • 如果是 toString(),hashCode(),equals()這幾個方法,那就是本地直接完成了。
  • 如果是真正的遠端呼叫,就會最後進入最後一行。
  1. 上一步之後再次step into,發現還是一個invoke方法,方法內,首先根據請求引數建立一個RequestTemplate,核心部分是 while(true) 裡面的 executeAndDecode(),while 其實是加了一層重試機制,這裡不多說。
  2. 進入executeAndDecode方法,我們看到 targetRequest() 構造出了一個request物件,而最終的response就是這個request請求的執行結果。

    同時我們能夠看到這個request物件的請求頭中是空的,當然也就不存在cookie,也就無法識別我們是否登入。

總結

  • 使用feign進行遠端呼叫時,首先判斷目標方法型別,如果是 toString(),hashCode(),equals()這幾個方法,那就是本地直接完成了;
  • 如果是真正的遠端呼叫,執行的是 executeAndDecode 方法,在這個方法體內,會通過 targetRequest 方法創建出一個新的 request 物件,這個新的request會按照我們指定的引數和路徑去傳送請求,並獲得響應結果。
  • 這個新的request物件的請求頭為空(所以會丟失原來的請求頭)

解決

問題在於feign自己創建出resttemplate,再用它構建一個新的request物件去傳送請求,而這個新的request不包含任何請求頭資訊。我們應該在它創造出這個request之後,在它真正傳送請求之前,把原始請求頭中的資料給它複製過去。

我們來看一下feign最後構建出建立request物件的 targetRequest方法

我們發現這裡面會有呼叫了一系列 RequestInterceptorapply方法對其進行增強,最後才返回,只不過預設情況下這些攔截器是空的。

因此 ,我們需要需要自己實現一個 RequestInterceptor,在它的apply方法中將原始請求頭中的資料同步到feign創建出的新的request中,並且將這個攔截器注入容器中,這樣feign在執行目標方法之前會被其攔截,對其先進行增強。

@Component
public class FeignBeforeExecInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();
        // 拿到原始請求頭資料
        String cookie = request.getHeader("Cookie");
        if (!StringUtils.isEmpty(cookie)) {
            // 同步
            template.header("Cookie", cookie);
        }
    }
}

比較難處理的地方在於,我們如何拿到原始的request物件,spring提供了一個叫 RequestContextHolder物件幫我們解決這個難題,通過它的 getRequestAttributes 方法或者 currentRequestAttributes 方法就能獲取到原始請求資料。關於這兩個方法的區別,可簡單認為,前者如果獲取失敗,會返回null;而後者會丟擲異常。

問題

還有個問題是這個 RequestContextHolder是如何儲存原始請求的,以至於我們在任何時候都能很方便的拿到,而不是像只能在controller層通過方法引數獲取。其實如果你細心看上面的原始碼圖片中的註釋的話,就能看到它寫的是獲取與當前執行緒繫結的請求資料

我們知道,伺服器(tomcat)會為每一個請求分配一個執行緒,從filter到controller到service到db再返回,全都都是同一個執行緒,所以,只要從一開始就把原始請求和這個執行緒繫結在一起,那麼只要在這個執行緒內,我就能隨時拿到這個資料。

是不是很熟悉,這不就是ThreadLocal嘛!再瞅一眼原始碼證明一下?