在平常的工作中,OpenFeign作為微服務(wù)間的調(diào)用組件使用的非常普遍,接口配合注解的調(diào)用方式突出一個(gè)簡(jiǎn)便,讓我們能無(wú)需關(guān)注內(nèi)部細(xì)節(jié)就能實(shí)現(xiàn)服務(wù)間的接口調(diào)用。
但是工作中用久了,發(fā)現(xiàn)Feign也有些使用起來(lái)麻煩的地方,下面先來(lái)看一個(gè)問(wèn)題,再看看我們?cè)诠ぷ髦惺侨绾谓鉀Q,以達(dá)到簡(jiǎn)化Feign使用的目的。
先看問(wèn)題
在一個(gè)項(xiàng)目開(kāi)發(fā)的過(guò)程中,我們通常會(huì)區(qū)分開(kāi)發(fā)環(huán)境、測(cè)試環(huán)境和生產(chǎn)環(huán)境,如果有的項(xiàng)目要求更高的話,可能還會(huì)有個(gè)預(yù)生產(chǎn)環(huán)境。
開(kāi)發(fā)環(huán)境作為和前端開(kāi)發(fā)聯(lián)調(diào)的環(huán)境,一般使用起來(lái)都比較隨意,而我們?cè)谶M(jìn)行本地開(kāi)發(fā)的時(shí)候,有時(shí)候也會(huì)將本地啟動(dòng)的微服務(wù)注冊(cè)到注冊(cè)中心nacos上,方便進(jìn)行調(diào)試。
這樣,注冊(cè)中心的一個(gè)微服務(wù)可能就會(huì)擁有多個(gè)服務(wù)實(shí)例,就像下面這樣:
眼尖的小伙伴肯定發(fā)現(xiàn)了,這兩個(gè)實(shí)例的ip地址有一點(diǎn)不同。
線上環(huán)境現(xiàn)在一般使用容器化部署,通常都是由流水線工具打成鏡像然后扔到docker中運(yùn)行,因此我們?nèi)タ匆幌路?wù)在docker容器內(nèi)的ip:
可以看到,這就是注冊(cè)到nacos上的服務(wù)地址之一,而列表中192開(kāi)頭的另一個(gè)ip,則是我們本地啟動(dòng)的服務(wù)的局域網(wǎng)地址。看一下下面這張圖,就能對(duì)整個(gè)流程一目了然了。
總結(jié)一下:
兩個(gè)service都是通過(guò)宿主機(jī)的ip和port,把自己的信息注冊(cè)到nacos上
線上環(huán)境的service注冊(cè)時(shí)使用docker內(nèi)部ip地址
本地的service注冊(cè)時(shí)使用本地局域網(wǎng)地址
那么這時(shí)候問(wèn)題就來(lái)了,當(dāng)我本地再啟動(dòng)一個(gè)serviceB,通過(guò)FeignClient來(lái)調(diào)用serviceA中的接口時(shí),因?yàn)镕eign本身的負(fù)載均衡,就可能把請(qǐng)求負(fù)載均衡到兩個(gè)不同的serviceA實(shí)例。
如果這個(gè)調(diào)用請(qǐng)求被負(fù)載均衡到本地serviceA的話,那么沒(méi)什么問(wèn)題,兩個(gè)服務(wù)都在同一個(gè)192.168網(wǎng)段內(nèi),可以正常訪問(wèn)。但是如果負(fù)載均衡請(qǐng)求到運(yùn)行在docker內(nèi)的serviceA的話,那么問(wèn)題來(lái)了,因?yàn)?a href="http://m.1cnz.cn/v/tag/1722/" target="_blank">網(wǎng)絡(luò)不通,所以會(huì)請(qǐng)求失敗:
說(shuō)白了,就是本地的192.168和docker內(nèi)的虛擬網(wǎng)段172.17屬于純二層的兩個(gè)不同網(wǎng)段,不能互訪,所以無(wú)法直接調(diào)用。
那么,如果想在調(diào)試時(shí)把請(qǐng)求穩(wěn)定打到本地服務(wù)的話,有一個(gè)辦法,就是指定在FeignClient中添加url參數(shù),指定調(diào)用的地址:
@FeignClient(value="serviceA",url="http://127.0.0.1:8088/") publicinterfaceClientA{ @GetMapping("/test/get") Stringget(); }
但是這么一來(lái)也會(huì)帶來(lái)點(diǎn)問(wèn)題:
代碼上線時(shí)需要再把注解中的url刪掉,還要再次修改代碼,如果忘了的話會(huì)引起線上問(wèn)題
如果測(cè)試的FeignClient很多的話,每個(gè)都需要配置url,修改起來(lái)很麻煩
那么,有什么辦法進(jìn)行改進(jìn)呢?為了解決這個(gè)問(wèn)題,我們還是得從Feign的原理說(shuō)起。
Feign原理
簡(jiǎn)單來(lái)說(shuō),就是項(xiàng)目中加的@EnableFeignClients這個(gè)注解,實(shí)現(xiàn)時(shí)有一行很重要的代碼:
@Import(FeignClientsRegistrar.class)
這個(gè)類實(shí)現(xiàn)了ImportBeanDefinitionRegistrar接口,在這個(gè)接口的registerBeanDefinitions方法中,可以手動(dòng)創(chuàng)建BeanDefinition并注冊(cè),之后spring會(huì)根據(jù)BeanDefinition實(shí)例化生成bean,并放入容器中。
Feign就是通過(guò)這種方式,掃描添加了@FeignClient注解的接口,然后一步步生成代理對(duì)象,具體流程可以看一下下面這張圖:
后續(xù)在請(qǐng)求時(shí),通過(guò)代理對(duì)象的FeignInvocationHandler進(jìn)行攔截,并根據(jù)對(duì)應(yīng)方法進(jìn)行處理器的分發(fā),完成后續(xù)的http請(qǐng)求操作。
ImportBeanDefinitionRegistrar
上面提到的ImportBeanDefinitionRegistrar,在整個(gè)創(chuàng)建FeignClient的代理過(guò)程中非常重要, 所以我們先寫(xiě)一個(gè)簡(jiǎn)單的例子看一下它的用法。先定義一個(gè)實(shí)體類:
@Data @AllArgsConstructor publicclassUser{ Longid; Stringname; }
通過(guò)BeanDefinitionBuilder,向這個(gè)實(shí)體類的構(gòu)造方法中傳入具體值,最后生成一個(gè)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方法的具體調(diào)用時(shí)間是在之后的ConfigurationClassPostProcessor執(zhí)行postProcessBeanDefinitionRegistry方法時(shí),而registerBeanDefinition方法則會(huì)將BeanDefinition放進(jìn)一個(gè)map中,后續(xù)根據(jù)它實(shí)例化bean。
在配置類上通過(guò)@Import將其引入:
@Configuration @Import(MyBeanDefinitionRegistrar.class) publicclassMyConfiguration{ }
注入這個(gè)User測(cè)試:
@Service @RequiredArgsConstructor publicclassUserService{ privatefinalUseruser; publicvoidgetUser(){ System.out.println(user.toString()); } }
結(jié)果打印,說(shuō)明我們通過(guò)自定義BeanDefinition的方式成功手動(dòng)創(chuàng)建了一個(gè)bean并放入了spring容器中:
User(id=1,name=Hydra)
好了,準(zhǔn)備工作鋪墊到這結(jié)束,下面開(kāi)始正式的改造工作。
改造
到這里先總結(jié)一下,我們糾結(jié)的點(diǎn)就是本地環(huán)境需要FeignClient中配置url,但線上環(huán)境不需要,并且我們又不想來(lái)回修改代碼。
除了像源碼中那樣生成動(dòng)態(tài)代理以及攔截方法,官方文檔中還給我們提供了一個(gè)手動(dòng)創(chuàng)建FeignClient的方法。
簡(jiǎn)單來(lái)說(shuō),就是我們可以像下面這樣,通過(guò)Feign的Builder API來(lái)手動(dòng)創(chuàng)建一個(gè)Feign客戶端。
簡(jiǎn)單看一下,這個(gè)過(guò)程中還需要配置Client、Encoder、Decoder、Contract、RequestInterceptor等內(nèi)容。
Client:實(shí)際http請(qǐng)求的發(fā)起者,如果不涉及負(fù)載均衡可以使用簡(jiǎn)單的Client.Default,用到負(fù)載均衡則可以使用LoadBalancerFeignClient,前面也說(shuō)了,LoadBalancerFeignClient中的delegate其實(shí)使用的也是Client.Default
Encoder和Decoder:Feign的編解碼器,在spring項(xiàng)目中使用對(duì)應(yīng)的SpringEncoder和ResponseEntityDecoder,這個(gè)過(guò)程中我們借用GsonHttpMessageConverter作為消息轉(zhuǎn)換器來(lái)解析json
RequestInterceptor:Feign的攔截器,一般業(yè)務(wù)用途比較多,比如添加修改header信息等,這里用不到可以不配
Contract:字面意思是合約,它的作用是將我們傳入的接口進(jìn)行解析驗(yàn)證,看注解的使用是否符合規(guī)范,然后將關(guān)于http的元數(shù)據(jù)抽取成結(jié)果并返回。如果我們使用RequestMapping、PostMapping、GetMapping之類注解的話,那么對(duì)應(yīng)使用的是SpringMvcContract
其實(shí)這里剛需的就只有Contract這一個(gè),其他都是可選的配置項(xiàng)。我們寫(xiě)一個(gè)配置類,把這些需要的東西都注入進(jìn)去:
@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(); ObjectFactory messageConverters=()->newHttpMessageConverters(httpMessageConverter); returnnewSpringEncoder(messageConverters); } }
在這個(gè)配置類上,還有三行注解,我們一點(diǎn)點(diǎn)解釋。
首先是引入的配置類LocalFeignProperties,里面有三個(gè)屬性,分別是是否開(kāi)啟本地路由的開(kāi)關(guān)、掃描FeignClient接口的包名,以及我們要做的本地路由映射關(guān)系,addressMapping中存的是服務(wù)名和對(duì)應(yīng)的url地址:
@Data @Component @ConfigurationProperties(prefix="feign.local") publicclassLocalFeignProperties{ //是否開(kāi)啟本地路由 privateStringenable; //掃描FeignClient的包名 privateStringbasePackage; //路由地址映射 privateMapaddressMapping; }
下面這行注解則表示只有當(dāng)配置文件中feign.local.enable這個(gè)屬性為true時(shí),才使當(dāng)前配置文件生效:
@ConditionalOnProperty(value="feign.local.enable",havingValue="true")
最后,就是我們重中之重的LocalFeignClientRegistrar了,我們還是按照官方通過(guò)ImportBeanDefinitionRegistrar接口構(gòu)建BeanDefinition然后注冊(cè)的思路來(lái)實(shí)現(xiàn)。
并且,F(xiàn)eignClientsRegistrar的源碼中已經(jīng)實(shí)現(xiàn)好了很多基礎(chǔ)的功能,比如掃掃描包、獲取FeignClient的name、contextId、url等等,所以需要改動(dòng)的地方非常少,可以放心的大抄特超它的代碼。
先創(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前的工作,這一部分主要完成了包的掃描和檢測(cè)@FeignClient注解是否被添加在接口上的測(cè)試。下面這段代碼基本上是照搬源碼,除了改動(dòng)一下掃描包的路徑,使用我們自己在配置文件中配置的包名。
@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"); Map attributes=annotationMetadata .getAnnotationAttributes(FeignClient.class.getCanonicalName()); Stringname=FeignCommonUtil.getClientName(attributes); registerFeignClient(registry,annotationMetadata,attributes); } } }
接下來(lái)創(chuàng)建BeanDefinition并注冊(cè),F(xiàn)eign的源碼中是使用的FeignClientFactoryBean創(chuàng)建代理對(duì)象,這里我們就不需要了,直接替換成使用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); Map addressMapping=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); }
在這個(gè)過(guò)程中主要做了這么幾件事:
通過(guò)beanFactory拿到了我們?cè)谇懊鎰?chuàng)建的Client、Encoder、Decoder、Contract,用來(lái)構(gòu)建Feign.Builder
通過(guò)注入配置類,通過(guò)addressMapping拿到配置文件中服務(wù)對(duì)應(yīng)的調(diào)用url
通過(guò)target方法替換要請(qǐng)求的url,如果配置文件中存在則優(yōu)先使用配置文件中url,否則使用@FeignClient注解中配置的url,如果都沒(méi)有則使用服務(wù)名通過(guò)LoadBalancerFeignClient訪問(wèn)
在resources/META-INF目錄下創(chuàng)建spring.factories文件,通過(guò)spi注冊(cè)我們的自動(dòng)配置類:
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.feign.local.config.FeignAutoConfiguration
最后,本地打包即可:
mvncleaninstall
測(cè)試
引入我們?cè)谏厦娲蚝玫陌捎诎幸呀?jīng)包含了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)建一個(gè)FeignClient接口,注解的url中我們可以隨便寫(xiě)一個(gè)地址,可以用來(lái)測(cè)試之后是否會(huì)被配置文件中的服務(wù)地址覆蓋:
@FeignClient(value="hydra-service", contextId="hydra-serviceA", url="http://127.0.0.1:8099/") publicinterfaceClientA{ @GetMapping("/test/get") Stringget(); @GetMapping("/test/user") UsergetUser(); }
啟動(dòng)服務(wù),過(guò)程中可以看見(jiàn)了執(zhí)行掃描包的操作:
在替換url過(guò)程中添加一個(gè)斷點(diǎn),可以看到即使在注解中配置了url,也會(huì)優(yōu)先被配置文件中的服務(wù)url覆蓋:
使用接口進(jìn)行測(cè)試,可以看到使用上面的代理對(duì)象進(jìn)行了訪問(wèn)并成功返回了結(jié)果:
如果項(xiàng)目需要發(fā)布正式環(huán)境,只需要將配置feign.local.enable改為false或刪掉,并在項(xiàng)目中添加Feign原始的@EnableFeignClients即可。
總結(jié)
本文提供了一個(gè)在本地開(kāi)發(fā)過(guò)程中簡(jiǎn)化Feign調(diào)用的思路,相比之前需要麻煩的修改FeignClient中的url而言,能夠節(jié)省不少的無(wú)效勞動(dòng),并且通過(guò)這個(gè)過(guò)程,也可以幫助大家了解我們平常使用的這些組件是怎么與spring結(jié)合在一起的,熟悉spring的擴(kuò)展點(diǎn)。
審核編輯:劉清
-
編解碼器
+關(guān)注
關(guān)注
0文章
261瀏覽量
24252 -
URL
+關(guān)注
關(guān)注
0文章
139瀏覽量
15373 -
虛擬機(jī)
+關(guān)注
關(guān)注
1文章
919瀏覽量
28279 -
HTTP接口
+關(guān)注
關(guān)注
0文章
21瀏覽量
1814
原文標(biāo)題:簡(jiǎn)化本地Feign調(diào)用,老手教你這么玩
文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論