1. 程式人生 > >【一起學原始碼-微服務】Feign 原始碼一:原始碼初探,通過Demo Debug Feign原始碼

【一起學原始碼-微服務】Feign 原始碼一:原始碼初探,通過Demo Debug Feign原始碼

前言

前情回顧

上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進行定時更新Eureka登錄檔資訊到BaseLoadBalancer中,預設30s排程一次。

本講目錄

這一講主要是講Fegin Demo以及通過入口註解@EnableFeignCliets和@FeignClient來進行原始碼初探。

目錄如下:

  1. Feign程式碼Demo
  2. Feign呼叫原理
  3. @EnableEurekaClient和@FeignClient註解掃描

說明

原創不易,如若轉載 請標明來源!

部落格地址:一枝花算不算浪漫
微信公眾號:壹枝花算不算浪漫

原始碼分析

Feign程式碼Demo

Fegin的Demo還是延續之前講解的Eureka的程式碼。地址為:
https://github.com/barrywangmeng/spring-cloud-learn


如上圖所示,ServiceB呼叫ServiceA的服務,定義了一個@FeignClient標註的ServiceAFeignClient介面,裡面定義了ServiceA中Controller提供的介面資訊。

Feign呼叫原理

接著我們可以啟動所有服務,呼叫ServiceBController,然後在serviceAFeignClient處打上斷點看一下:

我們發現這裡serviceAFeignClient顯示的是:ReflectiveFeign$FeignInvocationHandler@7642 ,這裡其實是使用了動態代理,因為ServiceAFeignClient 是一個介面,所以這裡可以猜測到底層使用的是JDK動態代理。

接著可以簡單地梳理下Feign請求簡單原理圖:

@EnableEurekaClient和@FeignClient註解掃描

先看下@EnableFeignClients註解程式碼:

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

    //等價於basePackages屬性,更簡潔的方式
    String[] value() default {};
    //指定多個包名進行掃描
    String[] basePackages() default {};

    //指定多個類或介面的class,掃描時會在這些指定的類和介面所屬的包進行掃描
    Class<?>[] basePackageClasses() default {};

     //為所有的Feign Client設定預設配置類
    Class<?>[] defaultConfiguration() default {};

     //指定用@FeignClient註釋的類列表。如果該項配置不為空,則不會進行類路徑掃描
    Class<?>[] clients() default {};
}

接著看下@FeignClient註解程式碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {

    //指定Feign Client的名稱,如果專案使用了 Ribbon,name屬性會作為微服務的名稱,用於服務發現
    @AliasFor("name")
    String value() default "";
    //用serviceId做服務發現已經被廢棄,所以不推薦使用該配置
    @Deprecated
    String serviceId() default "";
    //指定Feign Client的serviceId,如果專案使用了 Ribbon,將使用serviceId用於服務發現,但上面可以看到serviceId做服務發現已經被廢棄,所以也不推薦使用該配置
    @AliasFor("value")
    String name() default "";
    //為Feign Client 新增註解@Qualifier
    String qualifier() default "";
    //請求地址的絕對URL,或者解析的主機名
    String url() default "";
    //呼叫該feign client發生了常見的404錯誤時,是否呼叫decoder進行解碼異常資訊返回,否則丟擲FeignException
    boolean decode404() default false;
     //Feign Client設定預設配置類
    Class<?>[] configuration() default {};
    //定義容錯的處理類,當呼叫遠端介面失敗或超時時,會呼叫對應介面的容錯邏輯,fallback 指定的類必須實現@FeignClient標記的介面。實現的法方法即對應介面的容錯處理邏輯
    Class<?> fallback() default void.class;
    //工廠類,用於生成fallback 類示例,通過這個屬性我們可以實現每個介面通用的容錯邏輯,減少重複的程式碼
    Class<?> fallbackFactory() default void.class;
    //定義當前FeignClient的所有方法對映加統一字首
    String path() default "";
    //是否將此Feign代理標記為一個Primary Bean,預設為ture
    boolean primary() default true;
}

接著我們看下@EnableFeignClients中注入的
@Import(FeignClientsRegistrar.class) 原始碼:

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
        ResourceLoaderAware, EnvironmentAware {

    // patterned after Spring Integration IntegrationComponentScanRegistrar
    // and RibbonClientsConfigurationRegistgrar

    private ResourceLoader resourceLoader;

    private Environment environment;

    public FeignClientsRegistrar() {
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

   //在這個過載的方法裡面做了兩件事情:
   //1.將EnableFeignClients註解對應的配置屬性注入
   //2.將FeignClient註解對應的屬性注入
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        //注入EnableFeignClients註解對應的配置屬性
        registerDefaultConfiguration(metadata, registry);
       //注入FeignClient註解對應的屬性
        registerFeignClients(metadata, registry);
    }

   /**
   * 拿到 EnableFeignClients註解 defaultConfiguration 欄位的值
   * 然後進行注入
   *
   **/
    private void registerDefaultConfiguration(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        Map<String, Object> defaultAttrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

        if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
            String name;
            if (metadata.hasEnclosingClass()) {
                name = "default." + metadata.getEnclosingClassName();
            }
            else {
                name = "default." + metadata.getClassName();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }

    public void registerFeignClients(AnnotationMetadata metadata,
                                 BeanDefinitionRegistry registry) {
        // 獲取ClassPath掃描器
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        // 為掃描器設定資源載入器
        scanner.setResourceLoader(this.resourceLoader);

        Set<String> basePackages;
        // 1. 從@EnableFeignClients註解中獲取到配置的各個屬性值
        // 這裡可以獲取到配置的:basePackages=com.barrywang.service.feign
        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        // 2. 註解型別過濾器,只過濾@FeignClient   
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);
        // 3. 從1. 中的屬性值中獲取clients屬性的值        
        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            // 掃描器設定過濾器且獲取需要掃描的基礎包集合
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = getBasePackages(metadata);
        }else {
            // clients屬性值不為null,則將其clazz路徑轉為包路徑
            final Set<String> clientClasses = new HashSet<>();
            basePackages = new HashSet<>();
            for (Class<?> clazz : clients) {
                basePackages.add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }
            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                @Override
                protected boolean match(ClassMetadata metadata) {
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(
                    new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        }

        // 3. 掃描基礎包,且滿足過濾條件下的介面封裝成BeanDefinition
        for (String basePackage : basePackages) {
            // 找到basePackage下定義的@FeignClient介面列表
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            // 遍歷掃描到的bean定義        
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // 並校驗掃描到的bean定義類是一個介面
                    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());

                    String name = getClientName(attributes);
                    // 可以看到這裡也註冊了一個FeignClient的配置bean
                    // 這個方法是建立了一個FeignClientFactoryBean的工廠類,裡面儲存@FeignClient註解的所有屬性值
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));
                    // 註冊bean定義到spring中
                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

   /**
   * 註冊bean
   **/
    private void registerFeignClient(BeanDefinitionRegistry registry,
       AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        // 1.獲取類名稱,也就是本例中的FeignService介面
        String className = annotationMetadata.getClassName();

        // 2.BeanDefinitionBuilder的主要作用就是構建一個AbstractBeanDefinition
        // AbstractBeanDefinition類最終被構建成一個BeanDefinitionHolder
        // 然後註冊到Spring中
        // 注意:beanDefinition類為FeignClientFactoryBean,故在Spring獲取類的時候實際返回的是
        // FeignClientFactoryBean類
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
            .genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);

        // 3.新增FeignClientFactoryBean的屬性,
        // 這些屬性也都是我們在@FeignClient中定義的屬性
        definition.addPropertyValue("url", getUrl(attributes));
        definition.addPropertyValue("path", getPath(attributes));
        String name = getName(attributes);
        definition.addPropertyValue("name", name);
        definition.addPropertyValue("type", className);
        definition.addPropertyValue("decode404", attributes.get("decode404"));
        definition.addPropertyValue("fallback", attributes.get("fallback"));
        definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

        // 4.設定別名 name就是我們在@FeignClient中定義的name屬性
        String alias = name + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

        boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

        beanDefinition.setPrimary(primary);

        String qualifier = getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }

        // 5.定義BeanDefinitionHolder
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

    private void validate(Map<String, Object> attributes) {
        AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
        // This blows up if an aliased property is overspecified
        // FIXME annotation.getAliasedString("name", FeignClient.class, null);
        Assert.isTrue(
            !annotation.getClass("fallback").isInterface(),
            "Fallback class must implement the interface annotated by @FeignClient"
        );
        Assert.isTrue(
            !annotation.getClass("fallbackFactory").isInterface(),
            "Fallback factory must produce instances of fallback classes that implement the interface annotated by @FeignClient"
        );
    }

    private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
            Object configuration) {
        // 建立一個使用FeignClientSpecification構建的BeanDefinitionBuilder的類
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientSpecification.class);
        builder.addConstructorArgValue(name);
        builder.addConstructorArgValue(configuration);
        registry.registerBeanDefinition(
                name + "." + FeignClientSpecification.class.getSimpleName(),
                builder.getBeanDefinition());
    }

   ......
   ......
   ......
}

在這裡做了兩件事情:

  1. 將EnableFeignClients註解對應的配置屬性注入;
  2. 將FeignClient註解對應的屬性注入。

生成FeignClient對應的bean,注入到Spring 的IOC容器。

總結

在我們檢視處理@EnableFeignClients和@FeignClient註解的地方,最後呼叫registerFeignClient() 會構造一個:

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

所以我們後續會重點檢視FeignClientFactoryBean 這個類。

申明

本文章首發自本人部落格:https://www.cnblogs.com/wang-meng 和公眾號:壹枝花算不算浪漫,如若轉載請標明來源!

感興趣的小夥伴可關注個人公眾號:壹枝花算不算浪漫

相關推薦

一起原始碼-服務Feign 原始碼原始碼初探通過Demo Debug Feign原始碼

前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進

一起原始碼-服務Feign 原始碼Feign動態代理構造過程

前言 前情回顧 上一講主要看了@EnableFeignClients中的registerBeanDefinitions()方法,這裡面主要是 將EnableFeignClients註解對應的配置屬性注入,將FeignClient註解對應的屬性注入。 最後是生成FeignClient對應的bean,注入到Spr

一起原始碼-服務Feign 原始碼Feign結合Ribbon實現負載均衡的原理分析

前言 前情回顧 上一講我們已經知道了Feign的工作原理其實是在專案啟動的時候,通過JDK動態代理為每個FeignClinent生成一個動態代理。 動態代理的資料結構是:ReflectiveFeign.FeignInvocationHandler。其中包含target(裡面是serviceName等資訊)和d

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer啟動之配置檔案載入以及面向介面的配置項讀取

前言 上篇文章已經介紹了 為何要讀netflix eureka原始碼了,這裡就不再概述,下面開始正式原始碼解讀的內容。 如若轉載 請標明來源:一枝花算不算浪漫 程式碼總覽 還記得上文中,我們通過web.xml找到了eureka server入口的類EurekaBootStrap,這裡我們就先來簡單地看下: /

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer啟動之EurekaServer上下文EurekaClient建立

前言 上篇文章已經介紹了 Eureka Server 環境和上下文初始化的一些程式碼,其中重點講解了environment初始化使用的單例模式,以及EurekaServerConfigure基於介面對外暴露配置方法的設計方式。這一講就是講解Eureka Server上下文初始化剩下的內容:Eureka Cli

一起原始碼-服務Nexflix Eureka 原始碼在眼花繚亂的程式碼中EurekaClient是如何註冊的?

前言 上一講已經講解了EurekaClient的啟動流程,到了這裡已經有6篇Eureka原始碼分析的文章了,看了下之前的文章,感覺程式碼成分太多,會影響閱讀,後面會只擷取主要的程式碼,加上註釋講解。 這一講看的是EurekaClient註冊的流程,當然也是一塊核心,標題為什麼會寫上眼花繚亂呢?關於Eureka

一起原始碼-服務Nexflix Eureka 原始碼通過單元測試來Debug Eureka註冊過程

前言 上一講eureka client是如何註冊的,一直跟到原始碼傳送http請求為止,當時看eureka client註冊時如此費盡,光是找一個regiter的地方就找了半天,那麼client端傳送了http請求給server端,server端是如何處理的呢? 帶著這麼一個疑問 就開始今天原始碼的解讀了。

一起原始碼-服務Nexflix Eureka 原始碼EurekaClient登錄檔抓取 精妙設計分析!

前言 前情回顧 上一講 我們通過單元測試 來梳理了EurekaClient是如何註冊到server端,以及server端接收到請求是如何處理的,這裡最重要的關注點是登錄檔的一個數據結構:ConcurrentHashMap<String, Map<String, Lease<InstanceI

一起原始碼-服務Nexflix Eureka 原始碼服務續約原始碼分析

前言 前情回顧 上一講 我們講解了服務發現的相關邏輯,所謂服務發現 其實就是登錄檔抓取,服務例項預設每隔30s去註冊中心抓取一下注冊表增量資料,然後合併本地登錄檔資料,最後有個hash對比的操作。 本講目錄 今天主要是看下服務續約的邏輯,服務續約就是client端給server端傳送心跳檢測,告訴對方我還活著

一起原始碼-服務Nexflix Eureka 原始碼服務下線及例項摘除一個client下線到底多久才會被其他例項感知?

前言 前情回顧 上一講我們講了 client端向server端傳送心跳檢查,也是預設每30鍾傳送一次,server端接收後會更新登錄檔的一個時間戳屬性,然後一次心跳(續約)也就完成了。 本講目錄 這一篇有兩個知識點及一個疑問,這個疑問是在工作中真真實實遇到過的。 例如我有服務A、服務B,A、B都註冊在同一個註

一起原始碼-服務Nexflix Eureka 原始碼EurekaServer自我保護機制竟然有這麼多Bug?

前言 前情回顧 上一講主要講了服務下線,已經註冊中心自動感知宕機的服務。 其實上一講已經包含了很多EurekaServer自我保護的程式碼,其中還發現了1.7.x(1.9.x)包含的一些bug,但這些問題在master分支都已修復了。 服務下線會將服務例項從登錄檔中刪除,然後放入到recentQueue中,下

一起原始碼-服務Nexflix Eureka 原始碼十二EurekaServer叢集模式原始碼分析

前言 前情回顧 上一講看了Eureka 註冊中心的自我保護機制,以及裡面提到的bug問題。 哈哈 轉眼間都2020年了,這個系列的文章從12.17 一直寫到現在,也是不容易哈,每天持續不斷學習,輸出部落格,這一段時間確實收穫很多。 今天在公司給組內成員分享了Eureka原始碼剖析,反響效果還可以,也算是感覺收

一起原始碼-服務Nexflix Eureka 原始碼十三Eureka原始碼解讀完結撒花篇~!

前言 想說的話 【一起學原始碼-微服務-Netflix Eureka】專欄到這裡就已經全部結束了。 實話實說,從最開始Eureka Server和Eureka Client初始化的流程還是一臉悶逼,到現在Eureka各種操作都瞭然於心了。 本專欄從12.17開始寫,一直到今天12.30(文章在平臺是延後釋出的

一起原始碼-服務Ribbon 原始碼Ribbon概念理解及Demo除錯

前言 前情回顧 前面文章已經梳理清楚了Eureka相關的概念及原始碼,接下來開始研究下Ribbon的實現原理。 我們都知道Ribbon在spring cloud中擔當負載均衡的角色, 當兩個Eureka Client互相呼叫的時候,Ribbon能夠做到呼叫時的負載,保證多節點的客戶端均勻接收請求。(這個有點類

一起原始碼-服務Ribbon 原始碼通過Debug找出Ribbon初始化流程及ILoadBalancer原理分析

前言 前情回顧 上一講講了Ribbon的基礎知識,通過一個簡單的demo看了下Ribbon的負載均衡,我們在RestTemplate上加了@LoadBalanced註解後,就能夠自動的負載均衡了。 本講目錄 這一講主要是繼續深入RibbonLoadBalancerClient和Ribbon+Eureka整合的

一起原始碼-服務Ribbon 原始碼Ribbon與Eureka整合原理分析

前言 前情回顧 上一篇講了Ribbon的初始化過程,從LoadBalancerAutoConfiguration 到RibbonAutoConfiguration 再到RibbonClientConfiguration,我們找到了ILoadBalancer預設初始化的物件等。 本講目錄 這一講我們會進一步往下

一起原始碼-服務Ribbon 原始碼進一步探究Ribbon的IRule和IPing

前言 前情回顧 上一講深入的講解了Ribbon的初始化過程及Ribbon與Eureka的整合程式碼,與Eureka整合的類就是DiscoveryEnableNIWSServerList,同時在DynamicServerListLoadBalancer中會呼叫PollingServerListUpdater 進

一起原始碼-服務Ribbon原始碼Ribbon原始碼解讀彙總篇~

前言 想說的話 【一起學原始碼-微服務-Ribbon】專欄到這裡就已經全部結束了,共更新四篇文章。 Ribbon比較小巧,這裡是直接 讀的spring cloud 內嵌封裝的版本,裡面的各種configuration確實有點繞,不過看看第三講Ribbon初始化的過程總結圖就會清晰很多。 緊接著會繼續整理學習F

一起原始碼-服務Hystrix 原始碼Hystrix基礎原理與Demo搭建

說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一個系列文章講解了Feign的原始碼,主要是Feign動態代理實現的原理,及配合Ribbon實現負載均衡的機制。 這裡我們講解一個新的元件Hystrix,也是和Fe

一起原始碼-服務Hystrix 原始碼Hystrix核心流程Hystix非降級邏輯流程梳理

說明 原創不易,如若轉載 請標明來源! 歡迎關注本人微信公眾號:壹枝花算不算浪漫 更多內容也可檢視本人部落格:一枝花算不算浪漫 前言 前情回顧 上一講我們講了配置了feign.hystrix.enabled=true之後,預設的Targeter就會構建成HystrixTargter, 然後通過對應的Hystr