Feign原理 (圖解)

瘋狂創客圈 Java 高併發【 億級流量聊天室實戰】實戰系列 【博客園總入口html


瘋狂創客圈 正在進行分佈式和高併發基礎原理的研習,進行已經發布一些基礎性的文章:java

1、版本1 :springcloud + zookeeper 秒殺面試

以及有關Springcloud 幾篇核心、重要的文章spring

1、Springcloud 配置, 史上最全 一文全懂apache

2、Feign Ribbon Hystrix 三者關係 , 史上最全 深度解析api

3、SpringCloud gateway 詳解 , 史上最全性能優化

4、常識糾錯:Feign 默認不用 短鏈接服務器

1SpringCloud 中 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網絡請求。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-MpvoQNy0-1575176264529)(C:\Users\wuqinglin\AppData\Roaming\Typora\typora-user-images\1575175924885.png)]

​ 圖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 調用處理器。

具體,請關注 Java 高併發研習社羣博客園 總入口


最後,介紹一下瘋狂創客圈:瘋狂創客圈,一個Java 高併發研習社羣博客園 總入口

瘋狂創客圈,傾力推出:面試必備 + 面試必備 + 面試必備 的基礎原理+實戰 書籍 《Netty Zookeeper Redis 高併發實戰

img


瘋狂創客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰

  • Netty 源碼、原理、JAVA NIO 原理
  • Java 面試題 一網打盡
  • 瘋狂創客圈 【 博客園 總入口 】


m/crazymakercircle/p/9904544.html) 】

瘋狂創客圈,傾力推出:面試必備 + 面試必備 + 面試必備 的基礎原理+實戰 書籍 《Netty Zookeeper Redis 高併發實戰

[外鏈圖片轉存中...(img-QTCNgdff-1575176264531)]


瘋狂創客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰

  • Netty 源碼、原理、JAVA NIO 原理
  • Java 面試題 一網打盡
  • 瘋狂創客圈 【 博客園 總入口 】

相關文章
相關標籤/搜索