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 等組件的協作,感興趣的朋友可以自行下載源碼學習瞭解。

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