0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

簡化本地Feign調用的方法

jf_ro2CN3Fa ? 來源:碼農參上 ? 2023-06-20 10:01 ? 次閱讀

在平常的工作中,OpenFeign作為微服務間的調用組件使用的非常普遍,接口配合注解的調用方式突出一個簡便,讓我們能無需關注內部細節(jié)就能實現服務間的接口調用。

但是工作中用久了,發(fā)現Feign也有些使用起來麻煩的地方,下面先來看一個問題,再看看我們在工作中是如何解決,以達到簡化Feign使用的目的。

先看問題

在一個項目開發(fā)的過程中,我們通常會區(qū)分開發(fā)環(huán)境、測試環(huán)境和生產環(huán)境,如果有的項目要求更高的話,可能還會有個預生產環(huán)境。

開發(fā)環(huán)境作為和前端開發(fā)聯調的環(huán)境,一般使用起來都比較隨意,而我們在進行本地開發(fā)的時候,有時候也會將本地啟動的微服務注冊到注冊中心nacos上,方便進行調試。

這樣,注冊中心的一個微服務可能就會擁有多個服務實例,就像下面這樣:

7f82bc4c-0f0a-11ee-962d-dac502259ad0.png

眼尖的小伙伴肯定發(fā)現了,這兩個實例的ip地址有一點不同。

線上環(huán)境現在一般使用容器化部署,通常都是由流水線工具打成鏡像然后扔到docker中運行,因此我們去看一下服務在docker容器內的ip:

7f990bbe-0f0a-11ee-962d-dac502259ad0.png

可以看到,這就是注冊到nacos上的服務地址之一,而列表中192開頭的另一個ip,則是我們本地啟動的服務的局域網地址??匆幌孪旅孢@張圖,就能對整個流程一目了然了。

7fb393c6-0f0a-11ee-962d-dac502259ad0.jpg

總結一下:

兩個service都是通過宿主機的ip和port,把自己的信息注冊到nacos上

線上環(huán)境的service注冊時使用docker內部ip地址

本地的service注冊時使用本地局域網地址

那么這時候問題就來了,當我本地再啟動一個serviceB,通過FeignClient來調用serviceA中的接口時,因為Feign本身的負載均衡,就可能把請求負載均衡到兩個不同的serviceA實例。

如果這個調用請求被負載均衡到本地serviceA的話,那么沒什么問題,兩個服務都在同一個192.168網段內,可以正常訪問。但是如果負載均衡請求到運行在docker內的serviceA的話,那么問題來了,因為網絡不通,所以會請求失?。?/p>

7fd42a50-0f0a-11ee-962d-dac502259ad0.png

說白了,就是本地的192.168和docker內的虛擬網段172.17屬于純二層的兩個不同網段,不能互訪,所以無法直接調用。

那么,如果想在調試時把請求穩(wěn)定打到本地服務的話,有一個辦法,就是指定在FeignClient中添加url參數,指定調用的地址:

@FeignClient(value="serviceA",url="http://127.0.0.1:8088/")
publicinterfaceClientA{
@GetMapping("/test/get")
Stringget();
}

但是這么一來也會帶來點問題:

代碼上線時需要再把注解中的url刪掉,還要再次修改代碼,如果忘了的話會引起線上問題

如果測試的FeignClient很多的話,每個都需要配置url,修改起來很麻煩

那么,有什么辦法進行改進呢?為了解決這個問題,我們還是得從Feign的原理說起。

Feign原理

簡單來說,就是項目中加的@EnableFeignClients這個注解,實現時有一行很重要的代碼:

@Import(FeignClientsRegistrar.class)

這個類實現了ImportBeanDefinitionRegistrar接口,在這個接口的registerBeanDefinitions方法中,可以手動創(chuàng)建BeanDefinition并注冊,之后spring會根據BeanDefinition實例化生成bean,并放入容器中。

Feign就是通過這種方式,掃描添加了@FeignClient注解的接口,然后一步步生成代理對象,具體流程可以看一下下面這張圖:

7ff8fc4a-0f0a-11ee-962d-dac502259ad0.jpg

后續(xù)在請求時,通過代理對象的FeignInvocationHandler進行攔截,并根據對應方法進行處理器的分發(fā),完成后續(xù)的http請求操作。

ImportBeanDefinitionRegistrar

上面提到的ImportBeanDefinitionRegistrar,在整個創(chuàng)建FeignClient的代理過程中非常重要, 所以我們先寫一個簡單的例子看一下它的用法。先定義一個實體類:

@Data
@AllArgsConstructor
publicclassUser{
Longid;
Stringname;
}

通過BeanDefinitionBuilder,向這個實體類的構造方法中傳入具體值,最后生成一個BeanDefinition:

publicclassMyBeanDefinitionRegistrar
implementsImportBeanDefinitionRegistrar{
@Override
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,
BeanDefinitionRegistryregistry){
BeanDefinitionBuilderbuilder
=BeanDefinitionBuilder.genericBeanDefinition(User.class);
builder.addConstructorArgValue(1L);
builder.addConstructorArgValue("Hydra");

AbstractBeanDefinitionbeanDefinition=builder.getBeanDefinition();
registry.registerBeanDefinition(User.class.getSimpleName(),beanDefinition);
}
}

registerBeanDefinitions方法的具體調用時間是在之后的ConfigurationClassPostProcessor執(zhí)行postProcessBeanDefinitionRegistry方法時,而registerBeanDefinition方法則會將BeanDefinition放進一個map中,后續(xù)根據它實例化bean。

在配置類上通過@Import將其引入:

@Configuration
@Import(MyBeanDefinitionRegistrar.class)
publicclassMyConfiguration{
}

注入這個User測試:

@Service
@RequiredArgsConstructor
publicclassUserService{
privatefinalUseruser;

publicvoidgetUser(){
System.out.println(user.toString());
}
}

結果打印,說明我們通過自定義BeanDefinition的方式成功手動創(chuàng)建了一個bean并放入了spring容器中:

User(id=1,name=Hydra)

好了,準備工作鋪墊到這結束,下面開始正式的改造工作。

改造

到這里先總結一下,我們糾結的點就是本地環(huán)境需要FeignClient中配置url,但線上環(huán)境不需要,并且我們又不想來回修改代碼。

除了像源碼中那樣生成動態(tài)代理以及攔截方法,官方文檔中還給我們提供了一個手動創(chuàng)建FeignClient的方法。

簡單來說,就是我們可以像下面這樣,通過Feign的Builder API來手動創(chuàng)建一個Feign客戶端。

80198afa-0f0a-11ee-962d-dac502259ad0.png

簡單看一下,這個過程中還需要配置Client、Encoder、Decoder、Contract、RequestInterceptor等內容。

Client:實際http請求的發(fā)起者,如果不涉及負載均衡可以使用簡單的Client.Default,用到負載均衡則可以使用LoadBalancerFeignClient,前面也說了,LoadBalancerFeignClient中的delegate其實使用的也是Client.Default

Encoder和Decoder:Feign的編解碼器,在spring項目中使用對應的SpringEncoder和ResponseEntityDecoder,這個過程中我們借用GsonHttpMessageConverter作為消息轉換器來解析json

RequestInterceptor:Feign的攔截器,一般業(yè)務用途比較多,比如添加修改header信息等,這里用不到可以不配

Contract:字面意思是合約,它的作用是將我們傳入的接口進行解析驗證,看注解的使用是否符合規(guī)范,然后將關于http的元數據抽取成結果并返回。如果我們使用RequestMapping、PostMapping、GetMapping之類注解的話,那么對應使用的是SpringMvcContract

其實這里剛需的就只有Contract這一個,其他都是可選的配置項。我們寫一個配置類,把這些需要的東西都注入進去:

@Slf4j
@Configuration(proxyBeanMethods=false)
@EnableConfigurationProperties({LocalFeignProperties.class})
@Import({LocalFeignClientRegistrar.class})
@ConditionalOnProperty(value="feign.local.enable",havingValue="true")
publicclassFeignAutoConfiguration{
static{
log.info("feignlocalroutestarted");
}

@Bean
@Primary
publicContractcontract(){
returnnewSpringMvcContract();
}

@Bean(name="defaultClient")
publicClientdefaultClient(){
returnnewClient.Default(null,null);
}

@Bean(name="ribbonClient")
publicClientribbonClient(CachingSpringLoadBalancerFactorycachingFactory,
SpringClientFactoryclientFactory){
returnnewLoadBalancerFeignClient(defaultClient(),cachingFactory,
clientFactory);
}

@Bean
publicDecoderdecoder(){
HttpMessageConverterhttpMessageConverter=newGsonHttpMessageConverter();
ObjectFactorymessageConverters=()->newHttpMessageConverters(httpMessageConverter);
SpringDecoderspringDecoder=newSpringDecoder(messageConverters);
returnnewResponseEntityDecoder(springDecoder);
}

@Bean
publicEncoderencoder(){
HttpMessageConverterhttpMessageConverter=newGsonHttpMessageConverter();
ObjectFactorymessageConverters=()->newHttpMessageConverters(httpMessageConverter);
returnnewSpringEncoder(messageConverters);
}
}

在這個配置類上,還有三行注解,我們一點點解釋。

首先是引入的配置類LocalFeignProperties,里面有三個屬性,分別是是否開啟本地路由的開關、掃描FeignClient接口的包名,以及我們要做的本地路由映射關系,addressMapping中存的是服務名和對應的url地址:

@Data
@Component
@ConfigurationProperties(prefix="feign.local")
publicclassLocalFeignProperties{
//是否開啟本地路由
privateStringenable;

//掃描FeignClient的包名
privateStringbasePackage;

//路由地址映射
privateMapaddressMapping;
}

下面這行注解則表示只有當配置文件中feign.local.enable這個屬性為true時,才使當前配置文件生效:

@ConditionalOnProperty(value="feign.local.enable",havingValue="true")

最后,就是我們重中之重的LocalFeignClientRegistrar了,我們還是按照官方通過ImportBeanDefinitionRegistrar接口構建BeanDefinition然后注冊的思路來實現。

并且,FeignClientsRegistrar的源碼中已經實現好了很多基礎的功能,比如掃掃描包、獲取FeignClient的name、contextId、url等等,所以需要改動的地方非常少,可以放心的大抄特超它的代碼。

先創(chuàng)建LocalFeignClientRegistrar,并注入需要用到的ResourceLoader、BeanFactory、Environment。

@Slf4j
publicclassLocalFeignClientRegistrarimplements
ImportBeanDefinitionRegistrar,ResourceLoaderAware,
EnvironmentAware,BeanFactoryAware{

privateResourceLoaderresourceLoader;
privateBeanFactorybeanFactory;
privateEnvironmentenvironment;

@Override
publicvoidsetResourceLoader(ResourceLoaderresourceLoader){
this.resourceLoader=resourceLoader;
}

@Override
publicvoidsetBeanFactory(BeanFactorybeanFactory)throwsBeansException{
this.beanFactory=beanFactory;
}

@Override
publicvoidsetEnvironment(Environmentenvironment){
this.environment=environment;
}

//先省略具體功能代碼...
}

然后看一下創(chuàng)建BeanDefinition前的工作,這一部分主要完成了包的掃描和檢測@FeignClient注解是否被添加在接口上的測試。下面這段代碼基本上是照搬源碼,除了改動一下掃描包的路徑,使用我們自己在配置文件中配置的包名。

@Override
publicvoidregisterBeanDefinitions(AnnotationMetadataimportingClassMetadata,BeanDefinitionRegistryregistry){
ClassPathScanningCandidateComponentProviderscanner=ComponentScanner.getScanner(environment);
scanner.setResourceLoader(resourceLoader);
AnnotationTypeFilterannotationTypeFilter=newAnnotationTypeFilter(FeignClient.class);
scanner.addIncludeFilter(annotationTypeFilter);

StringbasePackage=environment.getProperty("feign.local.basePackage");
log.info("begintoscan{}",basePackage);

SetcandidateComponents=scanner.findCandidateComponents(basePackage);

for(BeanDefinitioncandidateComponent:candidateComponents){
if(candidateComponentinstanceofAnnotatedBeanDefinition){
log.info(candidateComponent.getBeanClassName());

//verifyannotatedclassisaninterface
AnnotatedBeanDefinitionbeanDefinition=(AnnotatedBeanDefinition)candidateComponent;
AnnotationMetadataannotationMetadata=beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClientcanonlybespecifiedonaninterface");

Mapattributes=annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());

Stringname=FeignCommonUtil.getClientName(attributes);
registerFeignClient(registry,annotationMetadata,attributes);
}
}
}

接下來創(chuàng)建BeanDefinition并注冊,Feign的源碼中是使用的FeignClientFactoryBean創(chuàng)建代理對象,這里我們就不需要了,直接替換成使用Feign.builder創(chuàng)建。

privatevoidregisterFeignClient(BeanDefinitionRegistryregistry,
AnnotationMetadataannotationMetadata,Mapattributes){
StringclassName=annotationMetadata.getClassName();
Classclazz=ClassUtils.resolveClassName(className,null);
ConfigurableBeanFactorybeanFactory=registryinstanceofConfigurableBeanFactory
?(ConfigurableBeanFactory)registry:null;
StringcontextId=FeignCommonUtil.getContextId(beanFactory,attributes,environment);
Stringname=FeignCommonUtil.getName(attributes,environment);

BeanDefinitionBuilderdefinition=BeanDefinitionBuilder
.genericBeanDefinition(clazz,()->{
Contractcontract=beanFactory.getBean(Contract.class);
ClientdefaultClient=(Client)beanFactory.getBean("defaultClient");
ClientribbonClient=(Client)beanFactory.getBean("ribbonClient");
Encoderencoder=beanFactory.getBean(Encoder.class);
Decoderdecoder=beanFactory.getBean(Decoder.class);

LocalFeignPropertiesproperties=beanFactory.getBean(LocalFeignProperties.class);
MapaddressMapping=properties.getAddressMapping();

Feign.Builderbuilder=Feign.builder()
.encoder(encoder)
.decoder(decoder)
.contract(contract);

StringserviceUrl=addressMapping.get(name);
StringoriginUrl=FeignCommonUtil.getUrl(beanFactory,attributes,environment);

Objecttarget;
if(StringUtils.hasText(serviceUrl)){
target=builder.client(defaultClient)
.target(clazz,serviceUrl);
}elseif(StringUtils.hasText(originUrl)){
target=builder.client(defaultClient)
.target(clazz,originUrl);
}else{
target=builder.client(ribbonClient)
.target(clazz,"http://"+name);
}

returntarget;
});

definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
FeignCommonUtil.validate(attributes);

AbstractBeanDefinitionbeanDefinition=definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE,className);

//hasadefault,won'tbenull
booleanprimary=(Boolean)attributes.get("primary");
beanDefinition.setPrimary(primary);

String[]qualifiers=FeignCommonUtil.getQualifiers(attributes);
if(ObjectUtils.isEmpty(qualifiers)){
qualifiers=newString[]{contextId+"FeignClient"};
}

BeanDefinitionHolderholder=newBeanDefinitionHolder(beanDefinition,className,
qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder,registry);
}

在這個過程中主要做了這么幾件事:

通過beanFactory拿到了我們在前面創(chuàng)建的Client、Encoder、Decoder、Contract,用來構建Feign.Builder

通過注入配置類,通過addressMapping拿到配置文件中服務對應的調用url

通過target方法替換要請求的url,如果配置文件中存在則優(yōu)先使用配置文件中url,否則使用@FeignClient注解中配置的url,如果都沒有則使用服務名通過LoadBalancerFeignClient訪問

在resources/META-INF目錄下創(chuàng)建spring.factories文件,通過spi注冊我們的自動配置類:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.feign.local.config.FeignAutoConfiguration

最后,本地打包即可:

mvncleaninstall

測試

引入我們在上面打好的包,由于包中已經包含了spring-cloud-starter-openfeign,所以就不需要再額外引feign的包了:


com.cn.hydra
feign-local-enhancer
1.0-SNAPSHOT

在配置文件中添加配置信息,啟用組件:

feign:
local:
enable:true
basePackage:com.service
addressMapping:
hydra-service:http://127.0.0.1:8088
trunks-service:http://127.0.0.1:8099

創(chuàng)建一個FeignClient接口,注解的url中我們可以隨便寫一個地址,可以用來測試之后是否會被配置文件中的服務地址覆蓋:

@FeignClient(value="hydra-service",
contextId="hydra-serviceA",
url="http://127.0.0.1:8099/")
publicinterfaceClientA{
@GetMapping("/test/get")
Stringget();

@GetMapping("/test/user")
UsergetUser();
}

啟動服務,過程中可以看見了執(zhí)行掃描包的操作:

803fe876-0f0a-11ee-962d-dac502259ad0.png

在替換url過程中添加一個斷點,可以看到即使在注解中配置了url,也會優(yōu)先被配置文件中的服務url覆蓋:

806f7668-0f0a-11ee-962d-dac502259ad0.png

使用接口進行測試,可以看到使用上面的代理對象進行了訪問并成功返回了結果:

809b35be-0f0a-11ee-962d-dac502259ad0.png

如果項目需要發(fā)布正式環(huán)境,只需要將配置feign.local.enable改為false或刪掉,并在項目中添加Feign原始的@EnableFeignClients即可。

總結

本文提供了一個在本地開發(fā)過程中簡化Feign調用的思路,相比之前需要麻煩的修改FeignClient中的url而言,能夠節(jié)省不少的無效勞動,并且通過這個過程,也可以幫助大家了解我們平常使用的這些組件是怎么與spring結合在一起的,熟悉spring的擴展點。





審核編輯:劉清

聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規(guī)問題,請聯系本站處理。 舉報投訴
  • 編解碼器
    +關注

    關注

    0

    文章

    250

    瀏覽量

    24196
  • URL
    URL
    +關注

    關注

    0

    文章

    139

    瀏覽量

    15297
  • 虛擬機
    +關注

    關注

    1

    文章

    904

    瀏覽量

    28018
  • HTTP接口
    +關注

    關注

    0

    文章

    21

    瀏覽量

    1768

原文標題:簡化本地Feign調用,老手教你這么玩

文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    labview屬性節(jié)點、本地變量和直接數據流不同調用方法速度

    1、直接數據流,速度最快2、本地變量,速度稍慢3、采用屬性節(jié)點Description,速度慢了一個數量級4、直接數據流+屬性節(jié)點只增加為兩者單獨調用時間的累加。5、屬性節(jié)點Value為什么比屬性節(jié)點
    發(fā)表于 11-14 17:13

    如何簡化程序框圖(方法)

    1.如何顯示子VI的部分控件于被調用該子VI的程序前面板上?2.程序框圖比較亂有哪些方法簡化?比如如何簡化程序框圖比較獨立的程序(自定義函數嗎?)
    發(fā)表于 04-05 14:17

    屬性節(jié)點、本地變量和直接數據流調用方法對速度的影響

    1、直接數據流,速度最快2、本地變量,速度稍慢3、采用屬性Description,速度慢了一個數量級4、直接數據流+屬性節(jié)點只增加為兩者單獨調用時間的累加。5、屬性節(jié)點Value為什么比屬性節(jié)點
    發(fā)表于 11-13 11:16

    matlab自定義函數調用方法

    matlab自定義函數調用方法 命令文件/函數文件+ 函數文件 - 多
    發(fā)表于 11-29 13:14 ?88次下載

    單片機系統(tǒng)中Web Service的調用方法研究

    本文介紹了一種在單片機系統(tǒng)中利用嵌入式網絡模塊實現Web Service調用方法,利用嵌入式網絡模塊實現串口到以太網數據的轉換,將串行數據封裝成Web Service請求包.它簡化了下位機和
    發(fā)表于 09-10 15:55 ?18次下載

    vb調用excel方法大全

    電子發(fā)燒友網站提供《vb調用excel方法大全.docx》資料免費下載
    發(fā)表于 04-14 10:27 ?6次下載

    Linux常見調用shell腳本的三種方法

    編寫Linux下的應用程序時有時需要調用Linux的相關shell腳本,在這些腳本中通過調用Linux的相關函數實現對應的功能。比如使用ifconfig配置本地的IP地址,采用這種方式省去了自己編寫應用程序去實現的麻煩。
    的頭像 發(fā)表于 06-28 14:28 ?8393次閱讀

    Oracle調用外部動態(tài)庫的設置方法

    Oracle調用外部動態(tài)庫的設置方法(電源技術及應用總結)-該文檔為Oracle調用外部動態(tài)庫的設置講解文檔,是一份不錯的參考資料,感興趣的可以先下載看看,,,,,,,,,,,,,
    發(fā)表于 09-28 13:57 ?12次下載
    Oracle<b class='flag-5'>調用</b>外部動態(tài)庫的設置<b class='flag-5'>方法</b>

    C調用matlab方法

    C調用matlab方法介紹
    發(fā)表于 07-31 10:55 ?0次下載

    feign調用常見問題避坑指南!

    摘要:主要是總結了一下這段時間在使用 feign 的過程中的遇到的一些坑點。
    的頭像 發(fā)表于 12-23 15:13 ?1907次閱讀

    動態(tài)Feign的“萬能”接口調用

    對于fegin調用,我們一般的用法都是為每個微服務都創(chuàng)建對應的feignclient接口,然后為每個微服務的controller接口,一一編寫對應的方法,去調用對應微服務的接口。
    發(fā)表于 12-26 11:42 ?3683次閱讀

    Feign第一次調用為什么會很慢?

    首先要了解Feign是如何進行遠程調用的,這里面包括,注冊中心、負載均衡、FeignClient之間的關系,微服務通過不論是eureka、nacos也好注冊到服務端,Feign是靠Ribbon做負載
    的頭像 發(fā)表于 08-17 15:00 ?1521次閱讀
    <b class='flag-5'>Feign</b>第一次<b class='flag-5'>調用</b>為什么會很慢?

    super調用父類的構造方法

    我們分析這句話“父類對象的引用”,那說明我們使用的時候只能在子類中使用,既然是對象的引用,那么我們也可以用來調用成員屬性以及成員方法,當然了,這里的 super 關鍵字還能夠調用父類的構造方法
    的頭像 發(fā)表于 10-10 16:42 ?858次閱讀
    super<b class='flag-5'>調用</b>父類的構造<b class='flag-5'>方法</b>

    什么是遠程過程調用

    )。 什么是遠程過程調用呢? 那么對于一個聊天系統(tǒng)有int send_information(int friend_id,string msg)這個方法,我們的一個處理邏輯是不是這樣: 調用bool
    的頭像 發(fā)表于 11-10 10:10 ?963次閱讀
    什么是遠程過程<b class='flag-5'>調用</b>

    Feign的超時時間如何設置呢?

    今天來聊一聊前段時間看到的一個面試題,也是在實際項目中需要考慮的一個問題,Feign 的超時時間如何設置?
    的頭像 發(fā)表于 11-15 10:22 ?1142次閱讀
    <b class='flag-5'>Feign</b>的超時時間如何設置呢?