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遠端呼叫之處打上斷點
- step into進入方法執行,會發現是一個代理物件的invoke方法在執行,首先判斷是方法名,
- 如果是 toString(),hashCode(),equals()這幾個方法,那就是本地直接完成了。
- 如果是真正的遠端呼叫,就會最後進入最後一行。
- 上一步之後再次step into,發現還是一個
invoke
方法,方法內,首先根據請求引數建立一個RequestTemplate
,核心部分是 while(true) 裡面的executeAndDecode()
,while 其實是加了一層重試機制,這裡不多說。
- 進入
executeAndDecode
方法,我們看到targetRequest()
構造出了一個request物件,而最終的response就是這個request請求的執行結果。
同時我們能夠看到這個request物件的請求頭中是空的,當然也就不存在cookie,也就無法識別我們是否登入。
總結
- 使用feign進行遠端呼叫時,首先判斷目標方法型別,如果是 toString(),hashCode(),equals()這幾個方法,那就是本地直接完成了;
- 如果是真正的遠端呼叫,執行的是
executeAndDecode
方法,在這個方法體內,會通過targetRequest
方法創建出一個新的 request 物件,這個新的request會按照我們指定的引數和路徑去傳送請求,並獲得響應結果。 - 這個新的request物件的請求頭為空(所以會丟失原來的請求頭)
解決
問題在於feign自己創建出resttemplate
,再用它構建一個新的request物件去傳送請求,而這個新的request不包含任何請求頭資訊。我們應該在它創造出這個request之後,在它真正傳送請求之前,把原始請求頭中的資料給它複製過去。
我們來看一下feign最後構建出建立request物件的 targetRequest
方法
我們發現這裡面會有呼叫了一系列 RequestInterceptor
的apply
方法對其進行增強,最後才返回,只不過預設情況下這些攔截器是空的。
因此 ,我們需要需要自己實現一個 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
嘛!再瞅一眼原始碼證明一下?