1. 程式人生 > 其它 >Spring Cloud 整合 Feign 的原理

Spring Cloud 整合 Feign 的原理

前言

上篇 介紹了 Feign 的核心實現原理,在文末也提到了會再介紹其和 Spring Cloud 的整合原理,Spring 具有很強的擴充套件性,會把一些常用的解決方案通過 starter 的方式開放給開發者使用,在引入官方提供的 starter 後通常只需要新增一些註解即可使用相關功能(通常是 @EnableXXX)。下面就一起來看看 Spring Cloud 到底是如何整合 Feign 的。

整合原理淺析

在 Spring 中一切都是圍繞 Bean 來展開的工作,而所有的 Bean 都是基於 BeanDefinition 來生成的,可以說 BeanDefinition 是整個 Spring 帝國的基石,這個整合的關鍵也就是要如何生成 Feign 對應的 BeanDefinition。

要分析其整合原理,我們首先要從哪裡入手呢?如果你看過 上篇 的話,在介紹結合 Spring Cloud 使用方式的例子時,第二步就是要在專案的 XXXApplication 上加新增 @EnableFeignClients 註解,我們可以從這裡作為切入點,一步步深入分析其實現原理(通常相當一部分的 starter 一般都是在啟動類中添加了開啟相關功能的註解)。

進入 @EnableFeignClients 註解中,其原始碼如下:

從註解的原始碼可以發現,該註解除了定義幾個引數(basePackages、defaultConfiguration、clients 等)外,還通過 @Import 引入了 FeignClientsRegistrar 類,一般 @Import 註解有如下功能(具體功能可見

官方 Java Doc):

  • 宣告一個 Bean
  • 匯入 @Configuration 註解的配置類
  • 匯入 ImportSelector 的實現類
  • 匯入 ImportBeanDefinitionRegistrar 的實現類(這裡使用這個功能

到這裡不難看出,整合實現的主要流程就在 FeignClientsRegistrar 類中了,讓我們繼續深入到類 FeignClientsRegistrar 的原始碼,

通過原始碼可知 FeignClientsRegistrar 實現 ImportBeanDefinitionRegistrar 介面,該介面從名字也不難看出其主要功能就是將所需要初始化的 BeanDefinition 注入到容器中,介面定義兩個方法功能都是用來注入給定的 BeanDefinition 的,一個可自定義 beanName(通過實現 BeanNameGenerator 介面自定義生成 beanName 的邏輯),另一個使用預設的規則生成 beanName(類名首字母小寫格式)。介面原始碼如下所示:

對 Spring 有一些瞭解的朋友們都知道,Spring 會在容器啟動的過程中根據 BeanDefinition 的屬性資訊完成對類的初始化,並注入到容器中。所以這裡 FeignClientsRegistrar 的終極目標就是將生成的代理類注入到 Spring 容器中。
雖然 FeignClientsRegistrar 這個類的原始碼看起來比較多,但是從其終結目標來看,我們主要是看如何生成 BeanDefinition 的,通過原始碼可以發現其實現了 ImportBeanDefinitionRegistrar 介面,並且重寫了 registerBeanDefinitions(AnnotationMetadata, BeanDefinitionRegistry) 方法,在這個方法裡完成了一些 BeanDefinition 的生成和註冊工作。原始碼如下:

整個過程主要分為如下兩個步驟:

  1. 給 @EnableFeignClients 的全域性預設配置(註解的 defaultConfiguration 屬性)建立 BeanDefinition 物件並注入到容器中(對應上圖中的第 ① 步)
  2. 給標有了 @FeignClient 的類建立 BeanDefinition 物件並注入到容器中(對應上圖中的第 ② 步)

下面分別深入方法原始碼實現來看其具體實現原理,首先來看看第一步的方法 registerDefaultConfiguration(AnnotationMetadata, BeanDefinitionRegistry),原始碼如下:

可以看到這裡只是獲取一下註解 @EnableFeignClients 的預設配置屬性 defaultConfiguration 的值,最終的功能實現交給了 registerClientConfiguration(BeanDefinitionRegistry, Object, Object) 方法來完成,繼續跟進深入該方法,其原始碼如下:

可以看到,全域性預設配置的 BeanClazz 都是 FeignClientSpecification,然後這裡將全域性預設配置 configuration 設定為 BeanDefinition 構造器的輸入引數,然後當呼叫構造器例項化時將這個引數傳進去。到這裡就已經把 @EnableFeignClients 的全域性預設配置(註解的 defaultConfiguration 屬性)創建出 BeanDefinition 物件並注入到容器中了,第一步到此完成,整體還是比較簡單的。

下面再來看看第二步 給標有了 @FeignClient 的類建立 BeanDefinition 物件並注入到容器中 是如何實現的。深入第二步的方法 registerFeignClients(AnnotationMetadata, BeanDefinitionRegistry) 實現中,由於方法實現程式碼較多,使用截圖會比較分散,所以用貼出原始碼並在相關位置新增必要註釋的方式進行:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 最終獲取到有 @FeignClient 註解類的集合
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
    // 獲取 @EnableFeignClients 註解的屬性 map
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    // 獲取 @EnableFeignClients 註解的 clients 屬性
    final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {  
        // 如果 @EnableFeignClients 註解未指定 clients 屬性則掃描新增(掃描過濾條件為:標註有 @FeignClient 的類)
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> basePackages = getBasePackages(metadata);
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    else {
        // 如果 @EnableFeignClients 註解已指定 clients 屬性,則直接新增,不再掃描(從這裡可以看出,為了加快容器啟動速度,建議都指定 clients 屬性)
        for (Class<?> clazz : clients) {
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    }

    // 遍歷最終獲取到的 @FeignClient 註解類的集合
    for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            // 驗證帶註釋的類必須是介面,不是介面則直接丟擲異常(大家可以想一想為什麼只能是介面?)
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
            // 獲取 @FeignClient 註解的屬性值
            Map<String, Object> attributes = annotationMetadata
                    .getAnnotationAttributes(FeignClient.class.getCanonicalName());
            // 獲取 clientName 的值,也就是在構造器的引數值(具體獲取邏輯可以參見 getClientName(Map<String, Object>) 方法      
            String name = getClientName(attributes);
            // 同上文第一步最後呼叫的方法,注入 @FeignClient 註解的配置物件到容器中
            registerClientConfiguration(registry, name, attributes.get("configuration"));
            // 注入 @FeignClient 物件,該物件可以在其它類中通過 @Autowired 直接引入(e.g. XXXService)
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

通過原始碼可以看到最後是通過方法 registerFeignClient(BeanDefinitionRegistry, AnnotationMetadata, Map<String, Object>) 注入的 @FeignClient 物件,繼續深入該方法,原始碼如下:

方法實現比較長,最終目標是構造出 BeanDefinition 物件,然後通過 BeanDefinitionReaderUtils.registerBeanDefinition(BeanDefinitionHolder, BeanDefinitionRegistry) 注入到容器中。其中關鍵的一步是從 @FeignClient 註解中獲取資訊並設定到 BeanDefinitionBuilder 中,BeanDefinitionBuilder 中註冊的類是 FeignClientFactoryBean,這個類的功能正如它的名字一樣是用來創建出 FeignClient 的 Bean 的,然後 Spring 會根據 FeignClientFactoryBean 生成物件並注入到容器中。

需要明確的一點是,實際上這裡最終注入到容器當中的是 FeignClientFactoryBean 這個類,Spring 會在類初始化的時候會根據這個類來生成例項物件,就是呼叫 FeignClientFactoryBean.getObject() 方法,這個生成的物件就是我們實際使用的代理物件。下面再進入到類 FeignClientFactoryBean 的 getObject() 這個⽅法,原始碼如下:

可以看到這個方法是直接呼叫的類中的另一個方法 getTarget() 的,在繼續跟進該方法,由於該方法實現程式碼較多,使用截圖會比較分散,所以用貼出原始碼並在相關位置新增必要註釋的方式進行:

/**
  * @param <T> the target type of the Feign client
  * @return a {@link Feign} client created with the specified data and the context
  * information
  */
<T> T getTarget() {
    // 從 Spring 容器中獲取 FeignContext Bean
    FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
            : applicationContext.getBean(FeignContext.class);
    // 根據獲取到的 FeignContext 構建出 Feign.Builder         
    Feign.Builder builder = feign(context);

    // 註解 @FeignClient 未指定 url 屬性 
    if (!StringUtils.hasText(url)) {
        // url 屬性是固定訪問某一個例項地址,如果未指定協議則拼接 http 請求協議
        if (!name.startsWith("http")) {
            url = "http://" + name;
        }
        else {
            url = name;
        }
        // 格式化 url
        url += cleanPath();
        // 生成代理和我們之前的代理一樣,註解 @FeignClient 未指定 url 屬性則返回一個帶有負載均衡功能的客戶端物件
        return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
    }
    // 註解 @FeignClient 已指定 url 屬性 
    if (StringUtils.hasText(url) && !url.startsWith("http")) {
        url = "http://" + url;
    }
    String url = this.url + cleanPath();
    // 獲取一個 client
    Client client = getOptional(context, Client.class);
    if (client != null) {
        if (client instanceof FeignBlockingLoadBalancerClient) {
            // not load balancing because we have a url,
            // but Spring Cloud LoadBalancer is on the classpath, so unwrap
            // 這裡沒有負載是因為我們有指定了 url 
            client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
        }
        builder.client(client);
    }
    // 生成代理和我們之前的代理一樣,最後被注入到 Spring 容器中
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

通過原始碼得知 FeignClientFactoryBean 繼承了 FactoryBean,其方法 FactoryBean.getObject 返回的就是 Feign 的代理物件,最後這個代理物件被注入到 Spring 容器中,我們就通過 @Autowired 可以直接注入使用了。同時還可以發現上面的程式碼分支最終都會走到如下程式碼:

Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, target);

點進去深入 targeter.target 的原始碼,可以看到實際上這裡建立的就是一個代理物件,也就是說在容器啟動的時候,會為每個 @FeignClient 建立了一個代理物件。至此,Spring Cloud 和 Feign 整合原理的核心實現介紹完畢。

總結

本文主要介紹了 Spring Cloud 整合 Feign 的原理。通過上文介紹,你已經知道 Srpring 會我們的標註的 @FeignClient 的介面建立了一個代理物件,那麼有了這個代理物件我們就可以做增強處理(e.g. 前置增強、後置增強),那麼你知道是如何實現的嗎?感興趣的朋友可以再翻翻原始碼尋找答案(溫馨提示:增強邏輯在 InvocationHandler 中)。還有 Feign 與 Ribbon 和 Hystrix 等元件的協作,感興趣的朋友可以自行下載原始碼學習瞭解。