歡迎關(guān)注我的github,以后所有文章源碼都會陸續(xù)更新上去
前提知識
我們知道在使用Feign的時候悯森,有三種方式可以實現(xiàn)自定義配置
- 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)原理
- @FeignClient configuration
在具體的@FeignClient上配置怨喘,此配置優(yōu)先級次之津畸,相同配置會被上一個覆蓋
image.png
Configuration配置類,此次注意必怜,此類不能有@Configuration注解肉拓,否則會被全局掃描到,變成了全局配置梳庆,此外方法上必須標(biāo)注@Bean才能生效
image.png
-
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)試工具
-
首先使用jad命令反編譯線上代碼是否正確
image.png
image.png
image.png - 確認(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 - 經(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)
此路走不通的話扁达,就又去github上看了下commit歷史記錄,果真發(fā)現(xiàn)一條
送上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é)如下:
- 如果你使用的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 {}
- 個人推薦方式莹桅,升級版本>=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這么多的配置都可以完全自定義了诈泼。