SpringCloud-技術專區-從原始碼層面讓你認識Feign工作流程和運作機制

李浩宇Alex發表於2021-08-18

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。

相關文章