【SpringCloud技術專題】「Fegin技術系列」從源碼層面讓你認識Feign工作流程和運作機制

Feign工作流程源碼解析

什么是feign:一款基于注解和動態代理的聲明式restful http客戶端。

原理

Feign發送請求實現原理

  • 微服務啟動類上標記@EnableFeignClients注解,然后Feign接口上標記@FeignClient注解。@FeignClient注解有幾個參數需要配置,這里不再贅述,都很簡單。

  • Feign框架會掃描注解,然后通過Feign類來處理注解,并最終生成一個Feign對象。

解析@FeignClient注解,生成MethodHandler

具體的解析類是ParseHandlerByName。這個類是ReflectiveFeign的內部類。

// 解析注解元數據,使用Contract解析
List<MethodMetadata> metadata = this.contract.parseAndValidateMetadata(key.type());

拿到注解元數據以后,循環處理注解元數據,創建每個方法對應的MethodHandler,這個MethodHandler最終會被代理對象調用。最終MethodHandler都會保存到下面這個集合中,然后返回。

Map<String, MethodHandler> result = new LinkedHashMap();
解析完成以后,調用ReflectiveFeign.newInstance()生成代理類。

MethodHandler是feign的一個接口,這個接口的invoke方法,是動態代理調用者InvocationHandler的invoke()方法最終調用的方法。

重新表述一遍:InvocationHandler的invoke()方法最終回調MethodHandler的invoke()來發送http請求。這就是Feign動態代理的具體實現。

ReflectiveFeign類的newInstance()方法的第57行:
// 創建動態代理調用者
InvocationHandler handler = this.factory.create(target, methodToHandler);
// 反射生成feign接口代理
T proxy = Proxy.newProxyInstance(加載器, 接口數組, handler);

InvocationHandler.invoke()的具體實現在FeignInvocationHandler.invoke(),FeignInvocationHandler也是ReflectiveFeign的一個內部類。里面有很多細節處理這里不再贅述,我們直接進入核心那一行代碼,以免影響思路,我們是理Feign的實現原理的!不要在意這些細節!

// InvocationHandler的invoke()方法最終回調MethodHandler的invoke()來發送http請求

ReflectiveFeign類的invoke()方法,第323行,代碼的后半段,如下:
(MethodHandler)this.dispatch.get(method). invoke(args);
  • this.dispatch:這是一個map,就是保存所有的MethodHandler的集合。參考創建InvocationHandler的位置:ReflectiveFeign類的newInstance()方法的第57行。

  • this.dispatch.get(method):這里的method就是我們開發者寫的feign接口中定義的方法的方法名!這段代碼的意思就是從MethodHandler集合中拿到我們需要調用的那個方法。

  • this.dispatch.get(method). invoke(args):這里的invoke就是調用的MethodHandler.invoke()!動態代理回調代理類,就這樣完成了,oh my god,多么偉大的創舉!

MethodHandler.invoke()的具體實現:SynchronousMethodHandler.invoke()

到了這里,就是發送請求的邏輯了。發送請求前,首先要創建請求模板,然后調用請求攔截器RequestInterceptor進行請求處理。

// 創建RequestTemplate
RequestTemplate template = this.buildTemlpateFromArgs.create(argv);
// 創建feign重試器,進行失敗重試
Retryer retryer = this.retryer.clone();
while(true){
    try{
        // 發送請求
        return this.executeAndDecode(template);
    } catch(RetryableException var5) {
        // 失敗重試,最多重試5次
        retryer.continueOrPropagate();
    }
}
RequestTemplate處理

RequestTemplate模板需要經過一系列攔截器的處理,主要有以下攔截器:

  • BasicAuthRequestInterceptor:授權攔截器,主要是設置請求頭的Authorization信息,這里是base64轉碼后的用戶名和密碼。

  • FeignAcceptGzipEncodingInterceptor:編碼類型攔截器,主要是設置請求頭的Accept-Encoding信息,默認值{gzip, deflate}。

  • FeignContextGzipEncodingInterceptor:壓縮格式攔截器,該攔截器會判斷請求頭中Context-Length屬性的值,是否大于請求內容的最大長度,如果超過最大長度2048,則設置請求頭的Context-Encoding信息,默認值{gzip, deflate}。注意,這里的2048是可以設置的,可以在配置文件中進行配置:

feign.compression.request.enabled=true
feign.compression.request.min-request-size=2048

min-request-size是通過FeignClientEncodingProperties來解析的,默認值是2048。

我們還可以自定義請求攔截器,我們自定義的攔截器,也會在此時進行調用,所有實現了RequestTemplate接口的類,都會在這里被調用。比如我們可以自定義攔截器把全局事務id放在請求頭里。

使用feign.Request把RequestTemplate包裝成feign.Request

feign.Request由5部分組成:

  • method

  • url

  • headers

  • body

  • charset

http請求客戶端

Feign發送http請求支持下面幾種http客戶端:

  • JDK自帶的HttpUrlConnection

  • Apache HttpClient

  • OkHttpClient

// 具體實現有2個類Client.Default 和LoadBalancerFeignClient

response = this.client.execute(request, this.options);

Client接口定義了execute()的接口,并且通過接口內部類實現了Client.execute()。

HttpURLConnection connection = this.convertAndSend(request, options);

return this.convertResponse(connection).toBuilder(). request(request).build();

  • 這里的Options定義了2個參數:

    • connectTimeoutMillis:連接超時時間,默認10秒。

    • readTimeoutMillis:讀取數據超時時間,默認60秒。

這種方式是最簡單的實現,但是不支持負載均衡,Spring Cloud整合了Feign和Ribbon,所以自然會把Feign和Ribbon結合起來使用。也就是說,Feign發送請求前,會先把請求再經過一層包裝,包裝成RibbonRequest。

也就是發送請求的另一種實現LoadBalancerFeignClient。

// 把Request包裝成RibbonRequest
RibbonRequest ribbonRequest = new   (this.delegate, request, uriWithoutHost);
// 配置超時時間
IClientConfig requestConfig = this.getClientConfig(options, clientName);
// 以負載均衡的方式發送請求
return ((RibbonResponse)this.IbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
以負載均衡的方式發送請求
  • this.IbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig))的具體實現在AbstractLoadBalancerAwareClient類中。

  • executeWithLoaderBalancer()方法的實現也參考了響應式編程,通過LoadBalancerCommand提交請求,然后使用Observable接收響應信息。

AbstractLoadBalancerAwareClient類的executeWithLoadBalancer()方法的第54行:

Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));

AbstractLoadBalancerAwareClient實現了IClient接口,該接口定義了execute()方法,

  • AbstractLoadBalancerAwareClient.this.execute()的具體實現有很多種:

    • OkHttpLoadBalancingClient

    • RetryableOkHttpLoadBalancingClient

    • RibbonLoadBalancingHttpClient

    • RetryableRibbonLoadBalancingHttpClient

我們以RibbonLoadBalancingHttpClient為例來說明,RibbonLoadBalancingHttpClient.execute()

第62行代碼:

// 組裝HttpUriRequest

HttpUriRequest httpUriRequest = request.toRequest(requestConfig);

// 發送http請求

HttpResponse httpResponse = ((HttpClient)this.delegate).execute(httpUriRequest);

// 使用RibbonApacheHttpResponse包裝http響應信息

return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI());

RibbonApacheHttpResponse由2部分組成:

httpResponse

uri

處理http相應

http請求經過上面一系列的轉發以后,最終還會回到SynchronousMethodHandler,然后SynchronousMethodHandler會進行一系列的處理,然后響應到瀏覽器。

  • 注冊Feign客戶端bean到IOC容器

  • 查看Feign框架源代碼,我們可以發現,FeignClientsRegistar的registerFeignClients()方法完成了feign相關bean的注冊。

Feign架構圖

新建位圖圖像.jpg
  • 第一步:基于JDK動態代理生成代理類。

  • 第二步:根據接口類的注解聲明規則,解析出底層MethodHandler

  • 第三步:基于RequestBean動態生成request。

  • 第四步:Encoder將bean包裝成請求。

  • 第五步:攔截器負責對請求和返回進行裝飾處理。

  • 第六步:日志記錄。

  • 第七步:基于重試器發送http請求,支持不同的http框架,默認使用的是HttpUrlConnection。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容