Spring Cloud Feign實現(xiàn)同一服務(wù)多client自定義配置

歡迎關(guān)注我的github,以后所有文章源碼都會陸續(xù)更新上去

前提知識

我們知道在使用Feign的時候悯森,有三種方式可以實現(xiàn)自定義配置

  1. properties
    直接在properties/yaml文件中配置屬性论咏,此配置優(yōu)先級最高
# xxx表示service name
feign.client.config.xxx.connectTimeout= 1000
feign.client.config.xxx.readTimeout = 3000

不過這里有一個很容易忽略的坑钓瞭,connectTimeout和readTimeout 必須同時配置才能生效,查看源碼可以發(fā)現(xiàn)原理

image.png

  1. @FeignClient configuration
    在具體的@FeignClient上配置怨喘,此配置優(yōu)先級次之津畸,相同配置會被上一個覆蓋
    image.png

    Configuration配置類,此次注意必怜,此類不能有@Configuration注解肉拓,否則會被全局掃描到,變成了全局配置梳庆,此外方法上必須標(biāo)注@Bean才能生效
    image.png
  1. global configuration
    直接在當(dāng)前項目包下新建一個@Configuration暖途,此配置優(yōu)先級最低,相同配置會被前兩個覆蓋


    image.png

理論上只要做好上面的配置膏执,就可以達(dá)到每個FeignClient任意自定義配置驻售。然而我們有小伙伴反饋線上后臺有一個很復(fù)雜的聚合查詢接口總是超過3.5S就報Read timed out,而根據(jù)上面截圖我們已經(jīng)做了自定義配置readTimeout=12000 ms更米,明顯沒有生效欺栗,這是為什么呢?

現(xiàn)場還原

為了保留現(xiàn)場征峦,我們直接使用阿里的Arthas線上診斷/調(diào)試工具

  1. 首先使用jad命令反編譯線上代碼是否正確


    image.png

    image.png

    image.png
  2. 確認(rèn)線上代碼沒有任何問題后迟几,在使用watch命令監(jiān)測實際方法執(zhí)行入?yún)ⅰS捎谖覀兪褂玫氖茿pacheHttpClient(其他實現(xiàn)方式如OkHttp類似),監(jiān)測如下方法即可
    image.png

    watch命令(由于線上已修復(fù)栏笆,故與前面截圖數(shù)值不完全一致类腮,當(dāng)時readTimeout=3500
    image.png
  3. 經(jīng)過上面兩步調(diào)試后,確定代碼沒有任何問題蛉加,但配置卻沒有生效存哲,所以問題應(yīng)該出現(xiàn)在Feign源碼執(zhí)行上因宇,開始擼源碼

源碼剖析

  • @EnableFeignClients,發(fā)現(xiàn)此注解上@Import了FeignClientsRegistrar
    image.png
  • FeignClientsRegistrar祟偷,注意此處執(zhí)行registerFeignClients方法
    image.png

    此方法內(nèi)掃描到每一個被@FeignClient注解的接口時會執(zhí)行三個很重要的動作
    1.getClientName 獲取client名稱察滑,一定要記住這個方法,后面會與另外一個getName方法放在一起對比修肠,這也是實現(xiàn)這邊文章標(biāo)題的關(guān)鍵
    2.registerClientConfiguration 注冊每一個client的上下文贺辰,這也是feign能夠?qū)崿F(xiàn)每一個client單獨配置的關(guān)鍵,具體實現(xiàn)不往下深挖嵌施,有興趣的可以自己去看源碼
    3.registerFeignClient 注冊每一個FeignClient代理實現(xiàn)類
    image.png
  • getClientName client這個map是由@FeignClient注解解析出來的饲化,首先獲取value值,如果為空則取name吗伤,如果name也為空吃靠,最后取serviceId(官方已建議Deprecated),再看我們的項目中只申明了@FeignClient(name=xxx)足淆,所以此方法必定返回xxx
    image.png
  • registerClientConfiguration 這里可以看到注冊的每一個configuraiton上下文的beanName其實就是等同于name.FeignClientSpecification巢块,configuration為@FeignClient(configuraiton=xxx)屬性,默認(rèn)為null巧号,再繼續(xù)深挖registerBeanDefinition方法
    image.png
  • DefaultListableBeanFactory.registerBeanDefinition 這已經(jīng)是spring很底層的方法了族奢,看到這兒就發(fā)現(xiàn)了自定義配置不生效的苗頭了。
    image.png

    原因由于在我們的項目中提供出來的API jar包中一個服務(wù)提供了很多接口丹鸿,因此我們進行了模塊劃分越走,造成了同一個service name提供了多個@FeignClient(這樣的場景應(yīng)該很普遍,一個@FeignClient上提供幾十上百個接口靠欢,體驗簡直太糟糕了)廊敌。
    再結(jié)合前面的getClientName方法可以得知每次執(zhí)行此方法返回的都是相同的beanName,那么這里配置類只會不斷的覆蓋门怪,configuration不為null的@FeignClient(name=xxx, configuration=yyy.class)被configuration為null的@FeignClient(name=xxx)所覆蓋骡澈,造成前面所提到的ApacheHttpClient無法獲取到自定義的configuration Options,降級為獲取parant context即全局的configuration Options.readTimeout=3500
  • registerFeignClient 上面解答了我們自定義配置為什么沒有生效的疑問薪缆,但是還沒有實現(xiàn)我們想要達(dá)到的同一服務(wù)名稱不同client的不同配置的目的秧廉。繼續(xù)往下看,這里又出現(xiàn)了一個getName方法
    image.png
  • getName 還記得前面提到的getClientName方法嗎拣帽?此處我們把兩個方法的代碼貼在一起對比看看有什么區(qū)別疼电,為什么要單獨寫一個方法?
    image.png

    image.png

    有沒有發(fā)現(xiàn)兩個方法取值順序剛好相反(service_id官方已經(jīng)不推薦了减拭,可以忽略蔽豺,我們只考慮name和value屬性),這里就給了我們實現(xiàn)文章標(biāo)題目的的機會:多個@FeignClient上使用相同的name拧粪,不同的value修陡,達(dá)到既可以同一個name發(fā)現(xiàn)服務(wù)沧侥,不同的value注冊不同的configuration,代碼如下
/**
* 品牌模塊魄鸦,無自定義配置宴杀,使用默認(rèn)全局配置
**/
@FeignClient(value = "brand", name = "goods")
public interface GoodsBrandFeign {}
/**
* 文章模塊
**/
@FeignClient(value = "article", name = "goods", configuration = ArticleFeignConfiguration.class)
public interface GoodsArticleFeign {}
/**
* 文章feign配置
**/
public class ArticleFeignConfiguration {
  @Bean
  public Options options() {
    return new Request.Options(1000, 20000);
  }
}
  • 原以為只需要配置不同value即可實現(xiàn),現(xiàn)實狠狠的打了我的臉拾因,運行拋出了異常旺罢,原來spring底層在解析注解的屬性輸出為Map時,內(nèi)部約束標(biāo)注了@AliasFor注解的屬性必須相同绢记,否則不允許通過
Caused by: org.springframework.core.annotation.AnnotationConfigurationException: In AnnotationAttributes for annotation [org.springframework.cloud.openfeign.FeignClient] declared on class 'com.epet.microservices.purchase.openapi.feign.TagBrandFeign', attribute 'name' and its alias 'value' are declared with values of [tag] and [brand], but only one is permitted.
    at org.springframework.core.annotation.AnnotationUtils.lambda$postProcessAnnotationAttributes$0(AnnotationUtils.java:1342)
    at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
    at org.springframework.core.annotation.AnnotationUtils.postProcessAnnotationAttributes(AnnotationUtils.java:1323)
    at org.springframework.core.annotation.AnnotationUtils.postProcessAnnotationAttributes(AnnotationUtils.java:1284)
    at org.springframework.core.type.classreading.AnnotationReadingVisitorUtils.convertClassValues(AnnotationReadingVisitorUtils.java:50)
    at org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor.getAnnotationAttributes(AnnotationMetadataReadingVisitor.java:142)
    at org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor.getAnnotationAttributes(AnnotationMetadataReadingVisitor.java:131)
    at org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor.getAnnotationAttributes(AnnotationMetadataReadingVisitor.java:51)
    at org.springframework.cloud.openfeign.FeignClientsRegistrar.registerFeignClients(FeignClientsRegistrar.java:151)
    at org.springframework.cloud.openfeign.FeignClientsRegistrar.registerBeanDefinitions(FeignClientsRegistrar.java:83)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:358)
    at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:357)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:146)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:118)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:328)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:233)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:271)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:91)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:692)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:530)
    at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:140)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:386)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1242)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1230)
    at com.epet.microservices.purchase.openapi.App.main(App.java:20)

image.png

此路走不通的話扁达,就又去github上看了下commit歷史記錄,果真發(fā)現(xiàn)一條
image.png

送上commit傳送門https://github.com/spring-cloud/spring-cloud-openfeign/commit/e227808b9d47c8c35d2a60414cb1c83564e72e5c
2.1.0.RELEASE版本之后@FeignClient新增了一個contextId屬性蠢熄,專門用于解決這個場景跪解,此次我們直接升級到spring cloud推薦版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR4</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
</dependencyManagement>
<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
</dependencies>
  • 查看最新代碼不難發(fā)現(xiàn),前面分析的源碼邏輯全都變更為根據(jù)contextId判斷了


    image.png

    image.png

    FeignClientFactoryBean也增加了contextId屬性签孔,上下文獲取邏輯也調(diào)整為根據(jù)contextId(contextId默認(rèn)不填等同于name)


    image.png

    image.png

結(jié)論

至此我們終于找到了最終完美解決@FeignClient相同name不同client不同配置的問題叉讥,總結(jié)如下:

  1. 如果你使用的spring-cloud-starter-openfeign是2.1.0之前的版本,是無法實現(xiàn)class(client)級別的細(xì)粒度自定義配置的骏啰,此種情況只能是多個class(client)共享同一個name节吮,同一份properties配置抽高,這是一直很粗粒度的配置判耕,只要是同一個服務(wù)名,所有的class(client)配置都一樣
feign.client.config.goods.connectTimeout = 3000
feign.client.config.goods.readTimeou = 3000
/**
* 商品服務(wù)品牌模塊
**/
@FeignClient(name = "goods", path = "brand")
public interface GoodsBrandFeign{}
/**
* 商品服務(wù)文章模塊
**/
@FeignClient(name = "goods", path = "article")
public interface GoodsArticle {}

需要格外提防在同一個服務(wù)名稱翘骂,多個class級別配置的configuration會因為名稱相同不為null的configuration被其他為null的configuration覆蓋而不生效壁熄,如下面的例子Options配置根本不會生效

public class GoodsBrandFeignConfiguration {
  @Bean
  public Options options() {
    return new Request.Options(4000, 4000);  
  }
}
/**
* 商品服務(wù)品牌模塊
**/
@FeignClient(name = "goods", path = "brand", configuration = GoodsBrandFeignConfiguration.class)
public interface GoodsBrandFeign{}
/**
* 商品服務(wù)文章模塊,因為此client configuration為空碳竟,而name與GoodsBrandFeign都為goods草丧,造成覆蓋了GoodsBrandFeignConfiguration配置,所以4000根本不會生效
**/
@FeignClient(name = "goods", path = "article")
public interface GoodsArticle {}
  1. 個人推薦方式莹桅,升級版本>=2.1.0昌执,最好參考spring cloud官網(wǎng)的release train
/**
* 商品品牌feign獨立配置
**/
public class GoodsBrandFeignConfiguration {
  @Bean
  public Options options() {
    return new Request.Options(4000, 4000);  
  }
}
/
/**
* 商品服務(wù)品牌模塊
**/
@FeignClient(name = "goods", contextId = "brand", path = "brand", configuration = GoodsBrandFeignConfiguration.class)
public interface GoodsBrandFeign{}

/**
* 商品文章feign獨立配置
**/
public class GoodsArticleFeignConfiguration {
  @Bean
  public Options options() {
    return new Request.Options(5000, 5000);  
  }
}
/
/**
* 商品服務(wù)文章模塊
**/
@FeignClient(name = "goods", contextId = "article", path = "article", configuration = GoodsArticleFeignConfiguration.class)
public interface GoodsArticleFeign {}

甚至還可以在properties中再次單獨覆蓋配置

feign:
  client:
    config:
      brand:
        connectTimeout: 7000
        readTimeout: 7000
      article:
        connectTimeout: 8000
        readTimeout: 8000

通過以上配置我們就可以做到了基于class(client)級別的細(xì)粒度任意獨立配置了,loggerLevel,retryer,errorDecoder,requestInterceptors,decode404,decoder,encoder,contract,exceptionPropagationPolicy這么多的配置都可以完全自定義了诈泼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懂拾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子铐达,更是在濱河造成了極大的恐慌岖赋,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瓮孙,死亡現(xiàn)場離奇詭異唐断,居然都是意外死亡选脊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門脸甘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恳啥,“玉大人,你說我怎么就攤上這事丹诀〗谴纾” “怎么了?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵忿墅,是天一觀的道長扁藕。 經(jīng)常有香客問我,道長疚脐,這世上最難降的妖魔是什么亿柑? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮棍弄,結(jié)果婚禮上望薄,老公的妹妹穿的比我還像新娘。我一直安慰自己呼畸,他們只是感情好痕支,可當(dāng)我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著蛮原,像睡著了一般卧须。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上儒陨,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天花嘶,我揣著相機與錄音,去河邊找鬼蹦漠。 笑死椭员,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的笛园。 我是一名探鬼主播隘击,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼研铆!你這毒婦竟也來了埋同?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蚜印,失蹤者是張志新(化名)和其女友劉穎莺禁,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窄赋,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡哟冬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年楼熄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片浩峡。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡可岂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翰灾,到底是詐尸還是另有隱情缕粹,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布纸淮,位于F島的核電站平斩,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咽块。R本人自食惡果不足惜绘面,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望侈沪。 院中可真熱鬧揭璃,春花似錦、人聲如沸亭罪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽应役。三九已至情组,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間扛吞,已是汗流浹背呻惕。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工荆责, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留滥比,地道東北人。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓做院,卻偏偏與公主長得像盲泛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子键耕,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,543評論 2 349