實戰系列-被面試官問到Feign原理

導語
  事情是這樣的,昨天參加了某公司二面,被面試官問道了Spring Cloud的RESTFul遠程調用。項目上用到的技術就是OpenFeign,面試官可能自己不是太瞭解,給他解釋一番發現自己還有很多的細節也不是太清楚,下面就來結合OpenFeign的源碼來分析一下

  Feign遠程調用,核心就是通過一系列的封裝和處理,將以JAVA註解的方式定義的遠程調用API接口,最終轉換成HTTP的請求形式,然後將HTTP的請求的響應結果,解碼成JAVA Bean,放回給調用者。

  在使用的時候我們必不可少的兩個註解@FeignClient和@EnableFeignClients兩個註解,一個是開啓Feign功能@EnableFeignClients ,一個是作爲客戶端應用注入@FeignClient。

@EnableFeignClients註解

  這個註解跟一般的註解無異,唯一不一樣的地方就是使用@Import註解,我們知道@Import註解是向容器中注入一些其他的或者第三方的對象,可以理解爲外部的操作通過Import的方式引入進來,既然是這樣,那就要研究一下這個引入的類。FeignClientsRegistrar,從單詞直譯上可以看出來其實它是一個註冊器。那麼這個註冊器到底有什麼作用。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	/**
	 * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
	 * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
	 * {@code @ComponentScan(basePackages="org.my.pkg")}.
	 * @return the array of 'basePackages'.
	 */
	String[] value() default {};

	/**
	 * Base packages to scan for annotated components.
	 * <p>
	 * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
	 * <p>
	 * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
	 * package names.
	 * @return the array of 'basePackages'.
	 */
	String[] basePackages() default {};

	/**
	 * Type-safe alternative to {@link #basePackages()} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * <p>
	 * Consider creating a special no-op marker class or interface in each package that
	 * serves no purpose other than being referenced by this attribute.
	 * @return the array of 'basePackageClasses'.
	 */
	Class<?>[] basePackageClasses() default {};

	/**
	 * A custom <code>@Configuration</code> for all feign clients. Can contain override
	 * <code>@Bean</code> definition for the pieces that make up the client, for instance
	 * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
	 *
	 * @see FeignClientsConfiguration for the defaults
	 * @return list of default configurations
	 */
	Class<?>[] defaultConfiguration() default {};

	/**
	 * List of classes annotated with @FeignClient. If not empty, disables classpath
	 * scanning.
	 * @return list of FeignClient classes
	 */
	Class<?>[] clients() default {};

}

  FeignClientsRegistrar ,這個類的描述是default ,表示只允許在同一個包中進行訪問。它所在的包空間是org.springframework.cloud.openfeign.FeignClientsRegistrar。也就是說org.springframework.cloud.openfeign包下的所有內容都可以訪問。那麼它的具體內容是怎麼樣子呢?

class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

  從代碼中可以看到它繼承了ResourceLoaderAware、EnvironmentAware以及ImportBeanDefinitionRegistrar。三個接口。這三個接口分別提供如下的一些操作

  • ResourceLoaderAware :設置資源加載擴展
  • EnvironmentAware:設置環境擴展
  • ImportBeanDefinitionRegistrar:通過註解信息,註冊一個BeanDefinition,通過註解操作向容器中注入Bean

  兩個驗證方法,分別驗證有沒有回調類和回調工廠類這個主要是用來檢測在@FeignClient參數中是否傳入了fallback參數和fallbackFactory參數。
在這裏插入圖片描述
在這裏插入圖片描述

  接下來其實關係的是ImportBeanDefinitionRegistrar接口的兩個實現方法

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry,
			BeanNameGenerator importBeanNameGenerator)

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
	}

  進入這個方法實現之後,會看到分別實現了兩個方法,默認的配置的注入,一個是對FeignClient的注入操作。首先來看看配置的注入
在這裏插入圖片描述

registerDefaultConfiguration()方法

  這個方法是私有方法傳入的參數分別是註解的元數據,以及BeanDefinition的註冊器。這個註冊器的使用在Spring擴展機制以及Spring源碼的分析中可以看到。 這裏首先獲取到一個默認參數,這裏調用的時候獲取的是EnableFeignClients註解中配置的信息,也就是說在EnableFeignClients註解中的那些配置項都有那些被配置了。
在這裏插入圖片描述
  會看到獲取完註解配置信息之後,第一步先去看看是否有 defaultConfiguration 的配置,如果有的話就去判斷是否是在其他地方被禁用了,如果沒有的話就加入默認配置,如果有的話就加入默認不配置。最終將所有的配置通過registerClientConfiguration()方法進行註冊。根據一般的Spring規則來看這裏並不是方法的終結,一般應該是有一個do開頭的方法纔會是最後的調用邏輯。既然這樣就進入到registerClientConfiguration()方法中。
在這裏插入圖片描述
  進入這個方法registerClientConfiguration(),會看到這裏採用了建造者模式,通過獲取到的基本信息要去組裝一個BeanDefinition出來。這裏卡看到調用了一個叫做FeignClientSpecification的類,通過這類去生成,也就是說這個就是最終需要定製生成的內容。到這裏就沒有啥可以分析了,後續的邏輯就是調用了Spring IOC的邏輯。

class FeignClientSpecification implements NamedContextFactory.Specification

  上面提到有兩個方法另個一方法registerFeignClients(metadata, registry);

registerFeignClients()方法

  從方法名上可以看到它注入的就是FeignClient。具體方法內容如下
在這裏插入圖片描述
  首先來分析一下整個的執行過程:

  • 1、進入方法之後獲取到一個類路徑掃描器,將資源加載器添加到掃描器中。
  • 2、分別的獲取到兩個註解的註解的源性信息。
  • 3、最終調用獲取組件的額方法將所有被註解標識的組件都獲取到
  • 4、調用了registerFeignClient(registry, annotationMetadata, attributes)方法進行注入

  從上面的邏輯分析可以看到,其實這裏面重要的一個方法就是registerFeignClient(registry, annotationMetadata, attributes),方法下面就來進入這個方法
  我們知道在Spring框架中BeanDefinition充當的角色就是整個的容器原料提供的角色,這個BeanDefinition並不是我們最終想要的或者說在應用中所需要的哪個Bean對象,而是爲我們最終想要的對象提供一個最原始的數據。那麼這裏會看到。通過構建者,這裏使用了我們的FactoryBean來構建出來一個特定的對象,而Spring的擴展機制就是通過FactoryBean來實現。這裏會看到這個構建者傳入的就是一個FeignClientFactoryBean,而他所繼承的接口就是FactoryBean的接口。而我們最終想從中獲取到的內容應該就被這個接口擴展過通過getObject獲取到的內容。而這個內容就是Builder組裝出來的內容。
在這裏插入圖片描述

  到這裏整個的裝配邏輯算是搞清楚了。從代碼上可以很清楚的看到FeignClient和EnableFeignClients是怎麼工作的。那麼既然搞清楚這些了接下來就來看看整個的具體的內部流轉邏輯。

內部流轉邏輯

  我們都知道在Spring Cloud Feign中使用的基於RESTFul風格的調用既然是RESTFul風格的調用。那麼就需要有一套基於RESTFul的調用邏輯。通過上面的邏輯可以知道,通過上面的注入操作,其實所有的被@FeignClient標註的內容已經全部進入到了IOC容器中。那麼接下來的內容就是怎麼樣去使用這些內容。

  也就是是說其實@FeignClient爲我們註冊進入的時候就已經是存在一個需要的最簡版本的BeanDefinition,而這個最簡版本正好可以滿足的通過動態代理進行調用的邏輯。也就是通過BeanDefinitionRegistry注入的內容。最終在我們當前運行的環境中應該有一個與遠程客戶端的IOC容器中相同的Bean定義內容。這個就是爲什麼可以在本地的應用中直接使用被@FeignClient標註的類進行調用。
  既然調用使用的是SpringIOC,那麼就有一個內容需要注意,就是從BeanDefinition到實際調用Bean的過程中其實是需要經過一層代理機制的。也就是說到使用之前我們並不知道具體需要一個什麼樣的Bean對象。而這裏在客戶端調用的被@FeignClient標註的內容就可以理解爲一個需要的Bean對象。

FeignInvocationHandler 調用處理器

  這裏就要從Spring容器的動態代理機制說起。有這樣一個邏輯,就是在不改變原有對象的基礎上,對原有對象進行操作,要滿足這樣的操作就需要使用我們的代理機制。

在這裏插入圖片描述
  通過 JDK Proxy 生成動態代理類,核心步驟就是需要定製一個調用處理器,具體來說,就是實現JDK中位於java.lang.reflect 包中的 InvocationHandler 調用處理器接口,並且實現該接口的 invoke(…) 抽象方法。在Feign的實現中是由下面這類來進行實現的FeignInvocationHandler。而這個類是數據才feign-core模塊中裏面feign.ReflectiveFeign類的一個靜態內部類。

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);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }

    @Override
    public boolean equals(Object obj) {
      if (obj instanceof FeignInvocationHandler) {
        FeignInvocationHandler other = (FeignInvocationHandler) obj;
        return target.equals(other.target);
      }
      return false;
    }

    @Override
    public int hashCode() {
      return target.hashCode();
    }

    @Override
    public String toString() {
      return target.toString();
    }
  }

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

  • (1)根據Java反射的方法實例,在dispatch 映射對象中,找到對應的MethodHandler 方法處理器;
  • (2)調用MethodHandler方法處理器的 invoke(…) 方法,完成實際的HTTP請求和結果的處理。

  補充說明一下:MethodHandler 方法處理器,和JDK 動態代理機制中位於 java.lang.reflect 包的 InvocationHandler 調用處理器接口,沒有任何的繼承和實現關係。MethodHandler 僅僅是Feign自定義的,一個非常簡單接口。
  上面這方法其中最關鍵的一點就是,有一個非常重要Map類型成員 dispatch 映射,保存着遠程接口方法到MethodHandler方法處理器的映射。
在這裏插入圖片描述
  默認的調用處理器 FeignInvocationHandler,在處理遠程方法調用的時候,會根據Java反射的方法實例,在dispatch 映射對象中,找到對應的MethodHandler 方法處理器,然後交給MethodHandler 完成實際的HTTP請求和結果的處理。

MethodHandler 方法處理器

  看上去這個方法處理器很簡單,但是我們需要有很多的不同的方法處理器,那麼這個地方使用到的就是工廠設計模式,這裏其實這個接口是被包括在一個InvocationHandlerFactory 的處理器工廠中這個工廠主要生產兩種處理器一個是上面的調用處理器,另一個就是方法處理器。

 /**
   * Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a
   * single method.
   */
  interface MethodHandler {

    Object invoke(Object[] argv) throws Throwable;
  }

  這個方法處理器默認有兩種處理方式一種是默認,一種是同步,既然這樣的話可以大膽的猜測,默認採用的異步的方式。
在這裏插入圖片描述

SynchronousMethodHandler 同步處理

  進入類型之後最爲關注的方法就是調用方法。invoke()調用方法
在這裏插入圖片描述

在這裏插入圖片描述
SynchronousMethodHandler的invoke(…)方法,調用了自己的executeAndDecode(…) 請求執行和結果解碼方法。該方法的工作步驟:

  • (1)首先通 RequestTemplate 請求模板實例,生成遠程URL請求實例 request;
  • (2)然後用自己的 feign 客戶端client成員,excecute(…) 執行請求,並且獲取 response 響應;
  • (3)對response 響應進行結果解碼

Feign 客戶端組件 feign.Client

 */
public interface Client {

  /**
   * Executes a request against its {@link Request#url() url} and returns a response.
   *
   * @param request safe to replay.
   * @param options options to apply to this request.
   * @return connected response, {@link Response.Body} is absent or unread.
   * @throws IOException on a network error connecting to {@link Request#url()}.
   */
  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 客戶端實現類。

在這裏插入圖片描述

Client.Default類

  這個類默認使用的自帶的HttpURLConnnection 來實現網絡請求。在JKD1.8中,雖然在HttpURLConnnection 底層,使用了非常簡單的HTTP連接池技術,但是,其HTTP連接的複用能力,實際是非常弱的,性能當然也很低

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 客戶端實現類型。

Feign 遠程調用的執行流程

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

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

在這裏插入圖片描述

總結

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

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

  上面說到,Feign在啓動時,會爲加上了@FeignClient註解的所有遠程接口創建一個本地JDK Proxy代理實例,並註冊到Spring IOC容器。在這裏,暫且將這個Proxy代理實例,叫做 DemoClientProxy,稍後,會詳細介紹這個Proxy代理實例的具體創建過程。
  然後,在UserController 調用代碼中,通過@Resource註解,按照類型或者名稱進行匹配,當然也可以使用其他方式。由於使用的SpringIOC所以還可以使用@Autowire 。從Spring IOC容器找到這個代理實例,並且裝配給@Resource註解所在的成員變量。也就是之前提到的被使用的FeginDemo
  例如,在需要代理進行hello()遠程調用時,直接通過 feginDemo 成員變量,調用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 調用處理器。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章