0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

简化本地Feign调用的方法

jf_ro2CN3Fa 来源:码农参上 2023-06-20 10:01 次阅读

在平常的工作中,OpenFeign作为微服务间的调用组件使用的非常普遍,接口配合注解的调用方式突出一个简便,让我们能无需关注内部细节就能实现服务间的接口调用。

但是工作中用久了,发现Feign也有些使用起来麻烦的地方,下面先来看一个问题,再看看我们在工作中是如何解决,以达到简化Feign使用的目的。

先看问题

在一个项目开发的过程中,我们通常会区分开发环境、测试环境和生产环境,如果有的项目要求更高的话,可能还会有个预生产环境。

开发环境作为和前端开发联调的环境,一般使用起来都比较随意,而我们在进行本地开发的时候,有时候也会将本地启动的微服务注册到注册中心nacos上,方便进行调试。

这样,注册中心的一个微服务可能就会拥有多个服务实例,就像下面这样:

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

眼尖的小伙伴肯定发现了,这两个实例的ip地址有一点不同。

线上环境现在一般使用容器化部署,通常都是由流水线工具打成镜像然后扔到docker中运行,因此我们去看一下服务在docker容器内的ip:

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

可以看到,这就是注册到nacos上的服务地址之一,而列表中192开头的另一个ip,则是我们本地启动的服务的局域网地址。看一下下面这张图,就能对整个流程一目了然了。

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

总结一下:

两个service都是通过宿主机的ip和port,把自己的信息注册到nacos上

线上环境的service注册时使用docker内部ip地址

本地的service注册时使用本地局域网地址

那么这时候问题就来了,当我本地再启动一个serviceB,通过FeignClient来调用serviceA中的接口时,因为Feign本身的负载均衡,就可能把请求负载均衡到两个不同的serviceA实例。

如果这个调用请求被负载均衡到本地serviceA的话,那么没什么问题,两个服务都在同一个192.168网段内,可以正常访问。但是如果负载均衡请求到运行在docker内的serviceA的话,那么问题来了,因为网络不通,所以会请求失败:

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

说白了,就是本地的192.168和docker内的虚拟网段172.17属于纯二层的两个不同网段,不能互访,所以无法直接调用。

那么,如果想在调试时把请求稳定打到本地服务的话,有一个办法,就是指定在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方法中,可以手动创建BeanDefinition并注册,之后spring会根据BeanDefinition实例化生成bean,并放入容器中。

Feign就是通过这种方式,扫描添加了@FeignClient注解的接口,然后一步步生成代理对象,具体流程可以看一下下面这张图:

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

后续在请求时,通过代理对象的FeignInvocationHandler进行拦截,并根据对应方法进行处理器的分发,完成后续的http请求操作。

ImportBeanDefinitionRegistrar

上面提到的ImportBeanDefinitionRegistrar,在整个创建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执行postProcessBeanDefinitionRegistry方法时,而registerBeanDefinition方法则会将BeanDefinition放进一个map中,后续根据它实例化bean。

在配置类上通过@Import将其引入:

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

注入这个User测试:

@Service
@RequiredArgsConstructor
publicclassUserService{
privatefinalUseruser;

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

结果打印,说明我们通过自定义BeanDefinition的方式成功手动创建了一个bean并放入了spring容器中:

User(id=1,name=Hydra)

好了,准备工作铺垫到这结束,下面开始正式的改造工作。

改造

到这里先总结一下,我们纠结的点就是本地环境需要FeignClient中配置url,但线上环境不需要,并且我们又不想来回修改代码。

除了像源码中那样生成动态代理以及拦截方法,官方文档中还给我们提供了一个手动创建FeignClient的方法。

简单来说,就是我们可以像下面这样,通过Feign的Builder API来手动创建一个Feign客户端。

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

简单看一下,这个过程中还需要配置Client、Encoder、Decoder、Contract、RequestInterceptor等内容。

Client:实际http请求的发起者,如果不涉及负载均衡可以使用简单的Client.Default,用到负载均衡则可以使用LoadBalancerFeignClient,前面也说了,LoadBalancerFeignClient中的delegate其实使用的也是Client.Default

Encoder和Decoder:Feign的编解码器,在spring项目中使用对应的SpringEncoder和ResponseEntityDecoder,这个过程中我们借用GsonHttpMessageConverter作为消息转换器来解析json

RequestInterceptor:Feign的拦截器,一般业务用途比较多,比如添加修改header信息等,这里用不到可以不配

Contract:字面意思是合约,它的作用是将我们传入的接口进行解析验证,看注解的使用是否符合规范,然后将关于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等等,所以需要改动的地方非常少,可以放心的大抄特超它的代码。

先创建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;
}

//先省略具体功能代码...
}

然后看一下创建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);
}
}
}

接下来创建BeanDefinition并注册,Feign的源码中是使用的FeignClientFactoryBean创建代理对象,这里我们就不需要了,直接替换成使用Feign.builder创建。

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拿到了我们在前面创建的Client、Encoder、Decoder、Contract,用来构建Feign.Builder

通过注入配置类,通过addressMapping拿到配置文件中服务对应的调用url

通过target方法替换要请求的url,如果配置文件中存在则优先使用配置文件中url,否则使用@FeignClient注解中配置的url,如果都没有则使用服务名通过LoadBalancerFeignClient访问

在resources/META-INF目录下创建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

创建一个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();
}

启动服务,过程中可以看见了执行扫描包的操作:

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

在替换url过程中添加一个断点,可以看到即使在注解中配置了url,也会优先被配置文件中的服务url覆盖:

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

使用接口进行测试,可以看到使用上面的代理对象进行了访问并成功返回了结果:

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

如果项目需要发布正式环境,只需要将配置feign.local.enable改为false或删掉,并在项目中添加Feign原始的@EnableFeignClients即可。

总结

本文提供了一个在本地开发过程中简化Feign调用的思路,相比之前需要麻烦的修改FeignClient中的url而言,能够节省不少的无效劳动,并且通过这个过程,也可以帮助大家了解我们平常使用的这些组件是怎么与spring结合在一起的,熟悉spring的扩展点。





审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 编解码器
    +关注

    关注

    0

    文章

    253

    浏览量

    24205
  • URL
    URL
    +关注

    关注

    0

    文章

    139

    浏览量

    15309
  • 虚拟机
    +关注

    关注

    1

    文章

    908

    浏览量

    28071
  • HTTP接口
    +关注

    关注

    0

    文章

    21

    浏览量

    1775

原文标题:简化本地Feign调用,老手教你这么玩

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    labview属性节点、本地变量和直接数据流不同调用方法速度

    1、直接数据流,速度最快2、本地变量,速度稍慢3、采用属性节点Description,速度慢了一个数量级4、直接数据流+属性节点只增加为两者单独调用时间的累加。5、属性节点Value为什么比属性节点
    发表于 11-14 17:13

    如何简化程序框图(方法)

    1.如何显示子VI的部分控件于被调用该子VI的程序前面板上?2.程序框图比较乱有哪些方法简化?比如如何简化程序框图比较独立的程序(自定义函数吗?)
    发表于 04-05 14:17

    属性节点、本地变量和直接数据流调用方法对速度的影响

    1、直接数据流,速度最快2、本地变量,速度稍慢3、采用属性Description,速度慢了一个数量级4、直接数据流+属性节点只增加为两者单独调用时间的累加。5、属性节点Value为什么比属性节点
    发表于 11-13 11:16

    matlab自定义函数调用方法

    matlab自定义函数调用方法 命令文件/函数文件+ 函数文件 - 多
    发表于 11-29 13:14 88次下载

    单片机系统中Web Service的调用方法研究

    本文介绍了一种在单片机系统中利用嵌入式网络模块实现Web Service调用方法,利用嵌入式网络模块实现串口到以太网数据的转换,将串行数据封装成Web Service请求包.它简化了下位机和
    发表于 09-10 15:55 18次下载

    vb调用excel方法大全

    电子发烧友网站提供《vb调用excel方法大全.docx》资料免费下载
    发表于 04-14 10:27 6次下载

    Linux常见调用shell脚本的三种方法

    编写Linux下的应用程序时有时需要调用Linux的相关shell脚本,在这些脚本中通过调用Linux的相关函数实现对应的功能。比如使用ifconfig配置本地的IP地址,采用这种方式省去了自己编写应用程序去实现的麻烦。
    的头像 发表于 06-28 14:28 8401次阅读

    Oracle调用外部动态库的设置方法

    Oracle调用外部动态库的设置方法(电源技术及应用总结)-该文档为Oracle调用外部动态库的设置讲解文档,是一份不错的参考资料,感兴趣的可以先下载看看,,,,,,,,,,,,,
    发表于 09-28 13:57 12次下载
    Oracle<b class='flag-5'>调用</b>外部动态库的设置<b class='flag-5'>方法</b>

    C调用matlab方法

    C调用matlab方法介绍
    发表于 07-31 10:55 0次下载

    feign调用常见问题避坑指南!

    摘要:主要是总结了一下这段时间在使用 feign 的过程中的遇到的一些坑点。
    的头像 发表于 12-23 15:13 1935次阅读

    动态Feign的“万能”接口调用

    对于fegin调用,我们一般的用法都是为每个微服务都创建对应的feignclient接口,然后为每个微服务的controller接口,一一编写对应的方法,去调用对应微服务的接口。
    发表于 12-26 11:42 3709次阅读

    Feign第一次调用为什么会很慢?

    首先要了解Feign是如何进行远程调用的,这里面包括,注册中心、负载均衡、FeignClient之间的关系,微服务通过不论是eureka、nacos也好注册到服务端,Feign是靠Ribbon做负载
    的头像 发表于 08-17 15:00 1538次阅读
    <b class='flag-5'>Feign</b>第一次<b class='flag-5'>调用</b>为什么会很慢?

    super调用父类的构造方法

    我们分析这句话“父类对象的引用”,那说明我们使用的时候只能在子类中使用,既然是对象的引用,那么我们也可以用来调用成员属性以及成员方法,当然了,这里的 super 关键字还能够调用父类的构造方法
    的头像 发表于 10-10 16:42 868次阅读
    super<b class='flag-5'>调用</b>父类的构造<b class='flag-5'>方法</b>

    什么是远程过程调用

    )。 什么是远程过程调用呢? 那么对于一个聊天系统有int send_information(int friend_id,string msg)这个方法,我们的一个处理逻辑是不是这样: 调用bool
    的头像 发表于 11-10 10:10 1003次阅读
    什么是远程过程<b class='flag-5'>调用</b>

    Feign的超时时间如何设置呢?

    今天来聊一聊前段时间看到的一个面试题,也是在实际项目中需要考虑的一个问题,Feign 的超时时间如何设置?
    的头像 发表于 11-15 10:22 1158次阅读
    <b class='flag-5'>Feign</b>的超时时间如何设置呢?