Feign原理 (圖解)


文章很長,建議收藏起來,慢慢讀! Java 高並發 發燒友社群:瘋狂創客圈 奉上以下珍貴的學習資源:


推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文




1 SpringCloud 中 Feign 核心原理

如果不了解 SpringCloud 中 Feign 核心原理,不會真正的了解 SpringCloud 的性能優化和配置優化,也就不可能做到真正掌握 SpringCloud。

本章從Feign 遠程調用的重要組件開始,圖文並茂的介紹 Feigh 遠程調用的執行流程、Feign 本地 JDK Proxy 實例的創建流程,徹底的為大家解讀 SpringCloud 的核心知識。使得廣大的工程師不光做到知其然,更能知其所以然。

1.1 簡介:Feign遠程調用的基本流程

Feign遠程調用,核心就是通過一系列的封裝和處理,將以JAVA注解的方式定義的遠程調用API接口,最終轉換成HTTP的請求形式,然后將HTTP的請求的響應結果,解碼成JAVA Bean,放回給調用者。Feign遠程調用的基本流程,大致如下圖所示。
在這里插入圖片描述

圖1 Feign遠程調用的基本流程

 

從上圖可以看到,Feign通過處理注解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的 Request 請求。通過Feign以及JAVA的動態代理機制,使得Java 開發人員,可以不用通過HTTP框架去封裝HTTP請求報文的方式,完成遠程服務的HTTP調用。

1.2 Feign 遠程調用的重要組件

在微服務啟動時,Feign會進行包掃描,對加@FeignClient注解的接口,按照注解的規則,創建遠程接口的本地JDK Proxy代理實例。然后,將這些本地Proxy代理實例,注入到Spring IOC容器中。當遠程接口的方法被調用,由Proxy代理實例去完成真正的遠程訪問,並且返回結果。

為了清晰的介紹SpringCloud中Feign運行機制和原理,在這里,首先為大家梳理一下Feign中幾個重要組件。

1.2.1 遠程接口的本地JDK Proxy代理實例

遠程接口的本地JDK Proxy代理實例,有以下特點:

(1)Proxy代理實例,實現了一個加 @FeignClient 注解的遠程調用接口;

(2)Proxy代理實例,能在內部進行HTTP請求的封裝,以及發送HTTP 請求;

(3)Proxy代理實例,能處理遠程HTTP請求的響應,並且完成結果的解碼,然后返回給調用者。

下面以一個簡單的遠程服務的調用接口 DemoClient 為例,具體介紹一下遠程接口的本地JDK Proxy代理實例的創建過程。

DemoClient 接口,有兩個非常簡單的遠程調用抽象方法:一個為hello() 抽象方法,用於完成遠程URL “/api/demo/hello/v1”的HTTP請求;一個為 echo(…) 抽象方法,用於完成遠程URL “/api/demo/echo/{word}/v1”的HTTP請求。具體如下圖所示。
在這里插入圖片描述

​ 圖2 遠程接口的本地JDK Proxy代理實例示意圖

DemoClient 接口代碼如下:

package com.crazymaker.springcloud.demo.contract.client; //…省略import @FeignClient( value = "seckill-provider", path = "/api/demo/", fallback = DemoDefaultFallback.class) public interface DemoClient { /** * 測試遠程調用 * * @return hello */ @GetMapping("/hello/v1") Result<JSONObject> hello(); /** * 非常簡單的一個 回顯 接口,主要用於遠程調用 * * @return echo 回顯消息 */ @RequestMapping(value = "/echo/{word}/v1", method = RequestMethod.GET) Result<JSONObject> echo( @PathVariable(value = "word") String word); }

注意,上面的代碼中,在DemoClient 接口上,加有@FeignClient 注解。也即是說,Feign在啟動時,會為其創建一個本地JDK Proxy代理實例,並注冊到Spring IOC容器。

如何使用呢?可以通過@Resource注解,按照類型匹配(這里的類型為DemoClient接口類型),從Spring IOC容器找到這個代理實例,並且裝配給需要的成員變量。

DemoClient的 本地JDK Proxy 代理實例的使用的代碼如下:

package com.crazymaker.springcloud.user.info.controller; //…省略import @Api(value = "用戶信息、基礎學習DEMO", tags = {"用戶信息、基礎學習DEMO"}) @RestController @RequestMapping("/api/user") public class UserController { @Resource DemoClient demoClient; //裝配 DemoClient 的本地代理實例 @GetMapping("/say/hello/v1") @ApiOperation(value = "測試遠程調用速度") public Result<JSONObject> hello() { Result<JSONObject> result = demoClient.hello(); JSONObject data = new JSONObject(); data.put("others", result); return Result.success(data).setMsg("操作成功"); } //… }

DemoClient的本地JDK Proxy代理實例的創建過程,比較復雜,稍后作為重點介紹。先來看另外兩個重要的邏輯組件。

1.2.2 調用處理器 InvocationHandler

大家知道,通過 JDK Proxy 生成動態代理類,核心步驟就是需要定制一個調用處理器,具體來說,就是實現JDK中位於java.lang.reflect 包中的 InvocationHandler 調用處理器接口,並且實現該接口的 invoke(…) 抽象方法。

為了創建Feign的遠程接口的代理實現類,Feign提供了自己的一個默認的調用處理器,叫做 FeignInvocationHandler 類,該類處於 feign-core 核心jar包中。當然,調用處理器可以進行替換,如果Feign與Hystrix結合使用,則會替換成 HystrixInvocationHandler 調用處理器類,類處於 feign-hystrix 的jar包中。
在這里插入圖片描述

​ 圖3 Feign中實現的 InvocationHandler 調用處理器

1.2.1 默認的調用處理器 FeignInvocationHandler

默認的調用處理器 FeignInvocationHandler 是一個相對簡單的類,有一個非常重要Map類型成員 dispatch 映射,保存着遠程接口方法到MethodHandler方法處理器的映射。

以前面示例中DemoClient 接口為例,其代理實現類的調用處理器 FeignInvocationHandler 的dispatch 成員的內存結構圖如圖3所示。
在這里插入圖片描述

​ 圖4 DemoClient代理實例的調用處理器 FeignInvocationHandler的dispatch 成員

為何在圖3中的Map類型成員 dispatch 映射對象中,有兩個Key-Value鍵值對呢?

原因是:默認的調用處理器 FeignInvocationHandle,在處理遠程方法調用的時候,會根據Java反射的方法實例,在dispatch 映射對象中,找到對應的MethodHandler 方法處理器,然后交給MethodHandler 完成實際的HTTP請求和結果的處理。前面示例中的 DemoClient 遠程調用接口,有兩個遠程調用方法,所以,其代理實現類的調用處理器 FeignInvocationHandler 的dispatch 成員,有兩個有兩個Key-Value鍵值對。

FeignInvocationHandler的關鍵源碼,節選如下:

package feign; //...省略import public class ReflectiveFeign extends Feign { //... //內部類:默認的Feign調用處理器 FeignInvocationHandler static class FeignInvocationHandler implements InvocationHandler { private final Target target; //方法實例對象和方法處理器的映射 private final Map<Method, MethodHandler> dispatch; //構造函數 FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) { this.target = checkNotNull(target, "target"); this.dispatch = checkNotNull(dispatch, "dispatch for %s", target); } //默認Feign調用的處理 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //... //首先,根據方法實例,從方法實例對象和方法處理器的映射中, //取得 方法處理器,然后,調用 方法處理器 的 invoke(...) 方法 return dispatch.get(method).invoke(args); } //... }

源碼很簡單,重點在於invoke(…)方法,雖然核心代碼只有一行,但是其功能是復雜的:

(1)根據Java反射的方法實例,在dispatch 映射對象中,找到對應的MethodHandler 方法處理器;

(2)調用MethodHandler方法處理器的 invoke(...) 方法,完成實際的HTTP請求和結果的處理。

補充說明一下:MethodHandler 方法處理器,和JDK 動態代理機制中位於 java.lang.reflect 包的 InvocationHandler 調用處理器接口,沒有任何的繼承和實現關系。MethodHandler 僅僅是Feign自定義的,一個非常簡單接口。

1.2.2 方法處理器 MethodHandler

Feign的方法處理器 MethodHandler 是一個獨立的接口,定義在 InvocationHandlerFactory 接口中,僅僅擁有一個invoke(…)方法,源碼如下:

//定義在InvocationHandlerFactory接口中 public interface InvocationHandlerFactory { //… //方法處理器接口,僅僅擁有一個invoke(…)方法 interface MethodHandler { //完成遠程URL請求 Object invoke(Object[] argv) throws Throwable; } //... }

MethodHandler 的invoke(…)方法,主要職責是完成實際遠程URL請求,然后返回解碼后的遠程URL的響應結果。Feign提供了默認的 SynchronousMethodHandler 實現類,提供了基本的遠程URL的同步請求處理。有關 SynchronousMethodHandler類以及其與MethodHandler的關系,大致如圖4所示。
在這里插入圖片描述

​ 圖5 Feign的MethodHandler方法處理器

為了徹底了解方法處理器,來讀一下 SynchronousMethodHandler 方法處理器的源碼,大致如下:

package feign; //…..省略import final class SynchronousMethodHandler implements MethodHandler { //… // 執行Handler 的處理 public Object invoke(Object[] argv) throws Throwable { RequestTemplate requestTemplate = this.buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer.clone(); while(true) { try { return this.executeAndDecode(requestTemplate); } catch (RetryableException var5) { //…省略不相干代碼 } } } //執行請求,然后解碼結果 Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = this.targetRequest(template); long start = System.nanoTime(); Response response; try { response = this.client.execute(request, this.options); response.toBuilder().request(request).build(); } } }

SynchronousMethodHandler的invoke(…)方法,調用了自己的executeAndDecode(…) 請求執行和結果解碼方法。該方法的工作步驟:

(1)首先通 RequestTemplate 請求模板實例,生成遠程URL請求實例 request;

(2)然后用自己的 feign 客戶端client成員,excecute(…) 執行請求,並且獲取 response 響應;

(3)對response 響應進行結果解碼。

1.2.3 Feign 客戶端組件 feign.Client

客戶端組件是Feign中一個非常重要的組件,負責端到端的執行URL請求。其核心的邏輯:發送request請求到服務器,並接收response響應后進行解碼。

feign.Client 類,是代表客戶端的頂層接口,只有一個抽象方法,源碼如下:

package feign; /**客戶端接口 * Submits HTTP {@link Request requests}. Implementations are expected to be thread-safe. */ public interface Client { //提交HTTP請求,並且接收response響應后進行解碼 Response execute(Request request, Options options) throws IOException; }

由於不同的feign.Client 實現類,內部完成HTTP請求的組件和技術不同,故,feign.Client 有多個不同的實現。這里舉出幾個例子:

(1)Client.Default類:默認的feign.Client 客戶端實現類,內部使用HttpURLConnnection 完成URL請求處理;

(2)ApacheHttpClient 類:內部使用 Apache httpclient 開源組件完成URL請求處理的feign.Client 客戶端實現類;

(3)OkHttpClient類:內部使用 OkHttp3 開源組件完成URL請求處理的feign.Client 客戶端實現類。

(4)LoadBalancerFeignClient 類:內部使用 Ribben 負載均衡技術完成URL請求處理的feign.Client 客戶端實現類。

此外,還有一些特殊場景使用的feign.Client客戶端實現類,也可以定制自己的feign.Client實現類。下面對上面幾個常見的客戶端實現類,進行簡要介紹。
在這里插入圖片描述

​ 圖6 feign.Client客戶端實現類

一:Client.Default類:

作為默認的Client 接口的實現類,在Client.Default內部使用JDK自帶的HttpURLConnnection類實現URL網絡請求。

圖片

​ 圖7 默認的Client 接口的客戶端實現類

在JKD1.8中,雖然在HttpURLConnnection 底層,使用了非常簡單的HTTP連接池技術,但是,其HTTP連接的復用能力,實際是非常弱的,性能當然也很低。具體的原因,參見后面的“SpringCloud與長連接的深入剖析”專題內容。

二:ApacheHttpClient類

ApacheHttpClient 客戶端類的內部,使用 Apache HttpClient開源組件完成URL請求的處理。

從代碼開發的角度而言,Apache HttpClient相比傳統JDK自帶的URLConnection,增加了易用性和靈活性,它不僅使客戶端發送Http請求變得容易,而且也方便開發人員測試接口。既提高了開發的效率,也方便提高代碼的健壯性。

從性能的角度而言,Apache HttpClient帶有連接池的功能,具備優秀的HTTP連接的復用能力。關於帶有連接池Apache HttpClient的性能提升倍數,具體可以參見后面的對比試驗。

ApacheHttpClient 類處於 feign-httpclient 的專門jar包中,如果使用,還需要通過Maven依賴或者其他的方式,倒入配套版本的專門jar包。

三:OkHttpClient類

OkHttpClient 客戶端類的內部,使用OkHttp3 開源組件完成URL請求處理。OkHttp3 開源組件由Square公司開發,用於替代HttpUrlConnection和Apache HttpClient。由於OkHttp3較好的支持 SPDY協議(SPDY是Google開發的基於TCP的傳輸層協議,用以最小化網絡延遲,提升網絡速度,優化用戶的網絡使用體驗。),從Android4.4開始,google已經開始將Android源碼中的 HttpURLConnection 請求類使用OkHttp進行了替換。也就是說,對於Android 移動端APP開發來說,OkHttp3 組件,是基礎的開發組件之一。

四:LoadBalancerFeignClient 類

LoadBalancerFeignClient 內部使用了 Ribben 客戶端負載均衡技術完成URL請求處理。在原理上,簡單的使用了delegate包裝代理模式:Ribben負載均衡組件計算出合適的服務端server之后,由內部包裝 delegate 代理客戶端完成到服務端server的HTTP請求;所封裝的 delegate 客戶端代理實例的類型,可以是 Client.Default 默認客戶端,也可以是 ApacheHttpClient 客戶端類或OkHttpClient 高性能客戶端類,還可以其他的定制的feign.Client 客戶端實現類型。

LoadBalancerFeignClient 負載均衡客戶端實現類,具體如下圖所示。
在這里插入圖片描述

​ 圖8 LoadBalancerFeignClient 負載均衡客戶端實現類

1.1 Feigh 遠程調用的執行流程

由於Feign遠程調用接口的JDK Proxy實例的InvokeHandler調用處理器有多種,導致Feign遠程調用的執行流程,也稍微有所區別,但是遠程調用執行流程的主要步驟,是一致的。這里主要介紹兩類JDK Proxy實例的InvokeHandler調用處理器相關的遠程調用執行流程:

(1)與 默認的調用處理器 FeignInvocationHandler 相關的遠程調用執行流程;

(2)與 Hystrix調用處理器 HystrixInvocationHandler 相關的遠程調用執行流程。

介紹過程中,還是以前面的DemoClient的JDK Proxy遠程動態代理實例的執行過程為例,演示分析Feigh遠程調用的執行流程。

1.1.1 與 FeignInvocationHandler 相關的遠程調用執行流程

FeignInvocationHandler是默認的調用處理器,如果不對Feign做特殊的配置,則Feign將使用此調用處理器。結合前面的DemoClient的JDK Proxy遠程動態代理實例的hello()遠程調用執行過程,在這里,詳細的介紹一下與 FeignInvocationHandler 相關的遠程調用執行流程,大致如下圖所示。
在這里插入圖片描述

​ 圖6 與 FeignInvocationHandler 相關的遠程調用執行流程

整體的遠程調用執行流程,大致分為4步,具體如下:

第1步:通過Spring IOC 容器實例,裝配代理實例,然后進行遠程調用。

前文講到,Feign在啟動時,會為加上了@FeignClient注解的所有遠程接口(包括 DemoClient 接口),創建一個本地JDK Proxy代理實例,並注冊到Spring IOC容器。在這里,暫且將這個Proxy代理實例,叫做 DemoClientProxy,稍后,會詳細介紹這個Proxy代理實例的具體創建過程。

然后,在本實例的UserController 調用代碼中,通過@Resource注解,按照類型或者名稱進行匹配(這里的類型為DemoClient接口類型),從Spring IOC容器找到這個代理實例,並且裝配給@Resource注解所在的成員變量,本實例的成員變量的名稱為 demoClient。

在需要代進行hello()遠程調用時,直接通過 demoClient 成員變量,調用JDK Proxy動態代理實例的hello()方法。

第2步:執行 InvokeHandler 調用處理器的invoke(…)方法

前面講到,JDK Proxy動態代理實例的真正的方法調用過程,具體是通過 InvokeHandler 調用處理器完成的。故,這里的DemoClientProxy代理實例,會調用到默認的FeignInvocationHandler 調用處理器實例的invoke(…)方法。

通過前面 FeignInvocationHandler 調用處理器的詳細介紹,大家已經知道,默認的調用處理器 FeignInvocationHandle,內部保持了一個遠程調用方法實例和方法處理器的一個Key-Value鍵值對Map映射。FeignInvocationHandle 在其invoke(…)方法中,會根據Java反射的方法實例,在dispatch 映射對象中,找到對應的 MethodHandler 方法處理器,然后由后者完成實際的HTTP請求和結果的處理。

所以在第2步中,FeignInvocationHandle 會從自己的 dispatch映射中,找到hello()方法所對應的MethodHandler 方法處理器,然后調用其 invoke(…)方法。

第3步:執行 MethodHandler 方法處理器的invoke(…)方法

通過前面關於 MethodHandler 方法處理器的非常詳細的組件介紹,大家都知道,feign默認的方法處理器為 SynchronousMethodHandler,其invoke(…)方法主要是通過內部成員feign客戶端成員 client,完成遠程 URL 請求執行和獲取遠程結果。

feign.Client 客戶端有多種類型,不同的類型,完成URL請求處理的具體方式不同。

第4步:通過 feign.Client 客戶端成員,完成遠程 URL 請求執行和獲取遠程結果

如果MethodHandler方法處理器實例中的client客戶端,是默認的 feign.Client.Default 實現類性,則使用JDK自帶的HttpURLConnnection類,完成遠程 URL 請求執行和獲取遠程結果。

如果MethodHandler方法處理器實例中的client客戶端,是 ApacheHttpClient 客戶端實現類性,則使用 Apache httpclient 開源組件,完成遠程 URL 請求執行和獲取遠程結果。

通過以上四步,應該可以清晰的了解到了 SpringCloud中的 feign 遠程調用執行流程和運行機制。

實際上,為了簡明扼要的介紹清楚默認的調用流程,上面的流程,實際上省略了一個步驟:第3步,實際可以分為兩小步。為啥呢? SynchronousMethodHandler 並不是直接完成遠程URL的請求,而是通過負載均衡機制,定位到合適的遠程server 服務器,然后再完成真正的遠程URL請求。換句話說,SynchronousMethodHandler實例的client成員,其實際不是feign.Client.Default類型,而是 LoadBalancerFeignClient 客戶端負載均衡類型。 因此,上面的第3步,如果進一步細分話,大致如下:(1)首先通過 SynchronousMethodHandler 內部的client實例,實質為負責客戶端負載均衡 LoadBalancerFeignClient 實例,首先查找到遠程的 server 服務端;(2) 然后再由LoadBalancerFeignClient 實例內部包裝的feign.Client.Default 內部類實例,去請求server端服務器,完成URL請求處理。

最后,說明下,默認的與 FeignInvocationHandler 相關的遠程調用執行流程,在運行機制以及調用性能上,滿足不了生產環境的要求,為啥呢? 大致原因有以下兩點:

(1) 沒有遠程調用過程中的熔斷監測和恢復機制;

(2) 也沒有用到高性能的HTTP連接池技術。

接下來,將為大家介紹一下用到熔斷監測和恢復機制 Hystrix 技術的遠程調用執行流程,該流程中,遠程接口的JDK Proxy動態代理實例所使用的調用處理器,叫做 HystrixInvocationHandler 調用處理器。

本文的內容,在《SpringCloud Nginx高並發核心編程》一書時,進行內容的完善和更新,並且進行的源碼的升級。 博客和書不一樣,書更加層層升入、層次分明,請大家以書的內容為准。

在這里插入圖片描述

具體,請關注 Java 高並發研習社群 【博客園 總入口 】


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM