Spring-Cloud之Feign原理剖析

vchar_fred發表於2021-06-01

Feign 主要是幫助我們方便進行rest api服務間的呼叫,其大體實現思路就我們通過標記註解在一個介面類上(註解上將包含要呼叫的介面資訊),之後在呼叫時根據註解資訊組裝好請求資訊,接下來基於ribbon這些負載均衡器來生成真實的服務地址,最後將請求傳送出去;之後將接收到的結果反序列化為相關的Java物件供我們直接使用。 下面我們走進Spring Cloud對feign封裝的原始碼中去了解其主要實現機制。

Feign的大體機制

通過在啟動類上標記 @EnableFeignClients 註解來開啟feign的功能,服務啟動後會掃描 @FeignClient 註解標記的介面,然後根據掃描的註解資訊為每個介面類生成feign客戶端請求,同時解析介面方法中的Spring MVC的相關注解,通過專門的註解解析器識別這些註解資訊,以便後面可以正確的組裝請求引數,使用 Ribbon 和 Eureka 獲取到請求服務的真實地址等資訊,最後使用 http 相關元件進行執行呼叫。其大致流程圖如下:

@EnableFeignClients 和 @FeignClient 註解

在EnableFeignClients 註解類中有一個 @Import(FeignClientsRegistrar.class)的配置

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 引入FeignClientsRegistrar 來掃描@FeignClient註解下的類
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    ...
}

我們追蹤程式碼進入到FeignClientsRegistrar類中,會發現FeignClientsRegistrar 類實現了ImportBeanDefinitionRegistrar(在spring context 專案中)介面,因此spring boot啟動時會呼叫它的registerBeanDefinitions()方法,該方法中會掃描 EnableFeignClients 和 FeignClient 註解資訊並設定相關資訊。

/**
 * spring boot 啟動時會自動呼叫 ImportBeanDefinitionRegistrar 入口方法
 */
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata
        , BeanDefinitionRegistry registry) {
    // 讀取 @EnableFeignClients 註解中資訊
    registerDefaultConfiguration(metadata, registry);
    // 掃描所有@FeignClient註解的類
    registerFeignClients(metadata, registry);
}

registerDefaultConfiguration方法

在registerDefaultConfiguration()方法中會讀取@EnableFeignClients註解資訊,然後將這些資訊註冊到一個 BeanDefinitionRegistry 裡面去;之後feign的一些預設配置將通過這裡註冊的資訊中取獲取。

registerFeignClients方法

  • registerFeignClients()方法會掃描相關包路徑(如果EnableFeignClients的basePackages沒有配置,預設會直接使用啟動類所在的包路徑)下所有的@FeiginClient註解的類
  • 然後根據@FeiginClient註解資訊向BeanDefinitionRegistry裡面註冊bean,注意這裡設定的bean名稱生成規則是使用服務名+FeignClientSpecification.class.getSimpleName(),因此如果對一個服務寫多個介面類會發生bean名稱重複導致註冊失敗。所以需要增加一個 allow-bean-definition-overriding: true 的配置。
  • 最後會呼叫 registerFeignClient() 方法註冊feign客戶端,這裡的bean名稱的為當前介面類的類路徑。

其流程圖如下:

feign客戶端的動態代理

上面registerFeignClient()方法中在構建bean的時候,實際構建的是FeignClientFactoryBean。

BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

FeignClientFactoryBean 類對父類的getObject()方法進行了重寫,後面動態代理時使用的就是它來獲取feign client的。在這裡會根據上面註解配置,同時會讀取application.yml配置資訊,根據配置來設定feign的相關資訊,比如編解碼器、註解解析器、請求超時時間等;之後如果沒有設定url那麼就會和負載均衡器(ribbon)整合。最後會通過反射將介面中相關方法進行解析儲存供後面進行jdk代理使用。

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

相關文章