Feign源碼解析二

前言

上文中彤悔,我們搭建了兩個服務嘉抓,一個user-service,一個order-service晕窑,來模擬利用FeignCliet發(fā)起遠程調用的實現(xiàn)

本文會基于Feign源碼抑片,看看Feign到底是怎么實現(xiàn)遠程調用

源碼

@EnableFeignClients注解

上文中,我們的user-service服務需要調用遠程的order-service服務完成一定的業(yè)務邏輯杨赤,而基本實現(xiàn)是order-service提供一個spi的jar包給user-service依賴敞斋,并且在user-service的啟動類上添加了一個注解

這個注解就是@EnableFeignClients截汪,接下來我們就從這個注解入手,一步一步解開Feign的神秘面紗

  • 類描述
/**
 * Scans for interfaces that declare they are feign clients (via
 * {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
 * Configures component scanning directives for use with
 * {@link org.springframework.context.annotation.Configuration}
 * <code>@Configuration</code> classes.
 *
 * @author Spencer Gibb
 * @author Dave Syer
 * @since 1.0
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
    ...
}

該注解類上的注釋大概的意思就是:
掃描那些被聲明為Feign Clients(只要有org.springframework.cloud.openfeign.FeignClient注解修飾的接口都是Feign Clients接口)的接口

  • 重要屬性
  1. basePackages
    值類型:String[]
    規(guī)定了掃描的基礎包位置
  2. clients
    值類型:Class<?>[]規(guī)定了掃描帶有@FeignClient注解的指定類全路徑Class的集合

下面我們繼續(xù)追蹤源碼植捎,看看到底什么地方用到了這個注解
利用IDEA的查找調用鏈快捷鍵衙解,可以發(fā)現(xiàn)在.class類型的文件中只有一個文件用到了這個注解

FeignClientsRegistrar.png

OK,下面主要就是看這個類做了什么


FeignClientsRegistrar.class

  • UML類圖
UML.png

通過UML圖我們發(fā)現(xiàn)該類分別實現(xiàn)了ImportBeanDefinitionRegistrar焰枢, ResourceLoaderAware以及EnvironmentAware接口
這三個接口均是spring-framework框架的spring-context模塊下的接口蚓峦,都是和spring上下文相關,具體作用下文會分析

  • 重要屬性(property
// 資源加載器济锄,可通過該資源加載器加載classpath下的所有文件
private ResourceLoader resourceLoader;

// 上下文環(huán)境暑椰,可通過該環(huán)境獲取當前應用配置屬性等
private Environment environment;

總結下來就是利用這兩個重要屬性,一個獲取應用配置屬性荐绝,一個可以加載classpath下的文件一汽,那么FeignClientsRegistrar持有這兩個東西之后要做什么呢?

  • 重要方法(Method
// 1. 初始化當前上下文環(huán)境屬性
@Override
public void setEnvironment(Environment environment) {
    this.environment = environment;
}

// 2. 初始化資源加載器屬性
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
    this.resourceLoader = resourceLoader;
}

// 3. 最重要的一個來了低滩,注冊bean定義
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 注冊默認配置
    registerDefaultConfiguration(metadata,registry);
    // 注冊FeignClients
    registerFeignClients(metadata, registry);
}
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拼接開頭
                name = "default." + metadata.getClassName();
            }
            registerClientConfiguration(registry, name,
                    defaultAttrs.get("defaultConfiguration"));
        }
    }
  1. 這里的注冊默認配置方法角虫,讀取啟動類上面 @EnableFeignClients注解中的defaultConfiguration,默認name為default委造,一般情況下無需配置戳鹅。用默認的FeignAutoConfiguration即可。 上面有個比較重要的方法:注冊配置registerClientConfiguration(...)昏兆,啟動流程一共有兩處讀取feign的配置枫虏,這是第一處。根據(jù)該方法看一下
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
            Object configuration) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientSpecification.class);
        builder.addConstructorArgValue(name);
        builder.addConstructorArgValue(configuration);
        registry.registerBeanDefinition(
                name + "." + FeignClientSpecification.class.getSimpleName(),
                builder.getBeanDefinition());
    }

上面將bean配置類包裝成FeignClientSpecification爬虱,注入到容器隶债。該對象非常重要,包含F(xiàn)eignClient需要的重試策略跑筝,超時策略死讹,日志等配置,如果某個FeignClient服務沒有設置獨立的配置類曲梗,則讀取默認的配置赞警,可以將這里注冊的bean理解為整個應用中所有feign的默認配置


  • 題外話:

由于FeignClientsRegistrar實現(xiàn)了ImportBeanDefinitionRegistrar接口,這里簡單提下這個接口的作用
我們知道在spring框架中虏两,我們如果想注冊一個bean的話主要由兩種方式:自動注冊/手動注冊

  1. 類上增加@Component愧旦,@Service,@Controller等注解定罢,由Spring自動幫我們注冊成bean

  2. 配置文件(.xml中增加<bean>...</bean>標簽)或配置類(@Configuration)中增加@Bean注解的類笤虫,也會由Spring幫我們自動注冊成bean

  3. 通過實現(xiàn)ImportBeanDefinitionRegistrar接口,并實現(xiàn)其registerBeanDefinitions方法,手動注冊bean

知道了ImportBeanDefinitionRegistrar接口的作用琼蚯,下面就來看下FeignClientsRegistrar類是何時被加載實例化的

通過IDEA工具搜索引用鏈酬凳,發(fā)現(xiàn)該類是在注解@EnableFeignClients上被import進來的,文章開始的圖片中有

@Import(FeignClientsRegistrar.class)

這里提下@Import注解的作用

/**
 * Indicates one or more {@link Configuration @Configuration} classes to import.
 *
 * <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
 * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
 * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
 * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
 *
 * <p>{@code @Bean} definitions declared in imported {@code @Configuration} classes should be
 * accessed by using {@link org.springframework.beans.factory.annotation.Autowired @Autowired}
 * injection. Either the bean itself can be autowired, or the configuration class instance
 * declaring the bean can be autowired. The latter approach allows for explicit, IDE-friendly
 * navigation between {@code @Configuration} class methods.
 *
 * <p>May be declared at the class level or as a meta-annotation.
 *
 * <p>If XML or other non-{@code @Configuration} bean definition resources need to be
 * imported, use the {@link ImportResource @ImportResource} annotation instead.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @since 3.0
 * @see Configuration
 * @see ImportSelector
 * @see ImportResource
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

/**
* {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
* or regular component classes to import.
*/
Class<?>[] value();
}

該注解僅有一個屬性value遭庶,使用該注解表明導入一個或者多個@Configuration類宁仔,其作用和.xml文件中的<import>等效,其允許導入@Configuration類罚拟,ImportSelector接口/ImportBeanDefinitionRegistrar接口的實現(xiàn)台诗,也同樣可以導入一個普通的組件類

注意完箩,如果是XML或非@Configuration的bean定義資源需要被導入的話赐俗,需要使用@ImportResource注解代替

這里我們導入的FeignClientsRegistrar類正是一個ImportBeanDefinitionRegistrar接口的實現(xiàn)

FeignClientsRegistrar重寫了該接口的registerBeanDefinitions方法,該方法有兩個參數(shù)注解元數(shù)據(jù)metadata和bean定義注冊表registry

該方法會由spring負責調用弊知,繼而注冊所有標注為@FeignClient注解的bean定義


registerFeignClients(...)

下面看registerBeanDefinitions方法中的第二個方法阻逮,在該方法中完成了所有@FeignClient注解接口的掃描工作,以及注冊到spring中秩彤,注意這里注冊bean的類型為FeignClientFactoryBean叔扼,下面細說

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

    Set<String> basePackages;
    // 1. 從@EnableFeignClients注解中獲取到配置的各個屬性值
    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) {
        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
                registerClientConfiguration(registry, name,
                        attributes.get("configuration"));
                // 注冊bean定義到spring中
                registerFeignClient(registry, annotationMetadata, attributes);
            }
        }
    }
}

總結一下該方法,就是掃描@EnableFeignClients注解上指定的basePackage或clients值降盹,獲取所有@FeignClient注解標識的接口,然后將這些接口一一調用以下兩個重要方法完成注冊configuration配置bean和注冊FeignClient bean

// ...省略部分代碼
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 獲取@FeignClient注解中指定的name
String name = getClientName(attributes);
// 1. 獲取@FeignClient注解中指定的configuration,并以FeignClientSpecification類注冊bean
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 2. 注冊FeignClient bean垄提,以FeignClientFactoryBean類注冊
registerFeignClient(registry, annotationMetadata, attributes);
  1. 可以理解為每個單獨的FeignClient接口都會注冊一個自己的configuration bean定義到spring的beanDefinitionMap中枫慷,key是@FeignClient注解上指定的serviceId/name/value值,value則是一個類型為FeignClientSpecification的BeanDefinition
  1. 中完成了每個FeignClient接口的client bean的注冊涡戳,beanDefinitionMap中的key是@FeignClient注解的每個接口的全限定名结蟋,value則是類型是FeignClientFactoryBean的BeanDefinition
registerFeignClient.png

斷點位置相當重要

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

這里是利用了spring的代理工廠來生成代理類,即這里將所有的 feignClient的描述信息BeanDefinition設定為 FeignClientFactoryBean類型渔彰,該類繼承自FactoryBean嵌屎,因此這是一個代理類,F(xiàn)actoryBean是一個工廠bean恍涂,用作創(chuàng)建代理bean编整,所以得出結論,feign將所有的feignClient bean定義的類型包裝成 FeignClientFactoryBean

最終其實就是存入了BeanFactory的beanDefinitionMap中

DefaultListableBeanFactory.png

那么代理類什么時候會觸發(fā)生成呢乳丰? 在spring刷新容器時掌测,會根據(jù)beanDefinition去實例化bean,如果beanDefinition的beanClass類型為代理bean,則會調用其T getObject() throws Exception;方法生成代理bean汞斧,而我們實際利用注入進來的FeignClient接口就是這些一個個代理類


總結

這里有坑...

這里有一個需要注意的點夜郁,也是開發(fā)中會遇到的一個啟動報錯點
如果我們同時定義了兩個不同名稱的接口(同一個包下/或依賴方指定全部掃描我們提供的@FeignClient),且這兩個@FeignClient接口注解的value/name/serviceId值一樣的話粘勒,依賴方拿到我們的提供的spi依賴竞端,啟動類上@EnableFeignClients注解掃描能同時掃描到這兩個接口,就會啟動報錯

原因就是Feign會為每個@FeignClient注解標識的接口都注冊一個以serviceId/name/value為key庙睡,F(xiàn)eignClientSpecification類型的bean定義為value去spring注冊bean定義事富,又默認不允許覆蓋bean定義,所以報錯

報錯信息.png

官方提示給出的解決方法要么改個@FeignClient注解的serviceId乘陪,name统台,value屬性值,要么就開啟spring允許bean定義覆寫

spring.main.allow-bean-definition-overriding=true

至此我們知道利用在springboot的啟動類上添加的@EnableFeignClients注解啡邑,該注解中import進來了一個手動注冊bean的FeignClientsRegistrar注冊器贱勃,該注冊器會由spring加載其registerBeanDefinitions方法,由此來掃描所有@EnableFeignClients注解定義的basePackages包路徑下的所有標注為@FeignClient注解的接口谤逼,并將其注冊到spring的bean定義Map中贵扰,并實例化bean

下一篇博文中,我會分析為什么我們在調用(@Resource)這些由@FeignClient注解的bean的方法時會發(fā)起遠程調用

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末流部,一起剝皮案震驚了整個濱河市戚绕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌枝冀,老刑警劉巖舞丛,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宾茂,居然都是意外死亡瓷马,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門跨晴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來欧聘,“玉大人,你說我怎么就攤上這事端盆』持瑁” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵焕妙,是天一觀的道長蒋伦。 經常有香客問我,道長焚鹊,這世上最難降的妖魔是什么痕届? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上研叫,老公的妹妹穿的比我還像新娘锤窑。我一直安慰自己,他們只是感情好嚷炉,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布渊啰。 她就那樣靜靜地躺著,像睡著了一般申屹。 火紅的嫁衣襯著肌膚如雪绘证。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天哗讥,我揣著相機與錄音嚷那,去河邊找鬼。 笑死忌栅,一個胖子當著我的面吹牛车酣,可吹牛的內容都是我干的曲稼。 我是一名探鬼主播索绪,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贫悄!你這毒婦竟也來了瑞驱?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤窄坦,失蹤者是張志新(化名)和其女友劉穎唤反,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸭津,經...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡彤侍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了逆趋。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盏阶。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖闻书,靈堂內的尸體忽然破棺而出名斟,到底是詐尸還是另有隱情,我是刑警寧澤魄眉,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布砰盐,位于F島的核電站,受9級特大地震影響坑律,放射性物質發(fā)生泄漏岩梳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望冀值。 院中可真熱鬧淘捡,春花似錦、人聲如沸池摧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽作彤。三九已至膘魄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間竭讳,已是汗流浹背创葡。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留绢慢,地道東北人灿渴。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像胰舆,于是被迫代替她去往敵國和親骚露。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

推薦閱讀更多精彩內容

  • ?通過前面兩章對Spring Cloud Ribbon和Spring Cloud Hystrix的介紹缚窿,我們已經掌...
    Chandler_玨瑜閱讀 213,025評論 15 140
  • 女人是不是花倦零、畫误续、詩:完全在于看她的人會不會養(yǎng)花,懂不懂品畫扫茅,能不能寫詩…
    厚臉臉閱讀 169評論 0 2
  • 初夏的一個傍晚蹋嵌,天空流動著淡淡的紅云,我端把椅子坐在院中葫隙,看兒子在地上玩耍栽烂。一切都那么安靜,那么祥和停蕉。 ...
    從明閱讀 665評論 2 4
  • Mac環(huán)境變量 文件路徑 環(huán)境變量 當要求系統(tǒng)運行一個程序而沒有告訴它程序所在的完整路徑時愕鼓,系統(tǒng)除了在當前目錄下面...
    壽_司閱讀 541評論 0 0