在平常的工作中,OpenFeign
作为微服务间的调用组件使用的非常普遍,接口配合注解的调用方式突出一个简便,让我们能无需关注内部细节就能实现服务间的接口调用。
但是工作中用久了,发现Feign也有些使用起来麻烦的地方,下面先来看一个问题,再看看我们在工作中是如何解决,以达到简化Feign使用的目的。
先看问题
在一个项目开发的过程中,我们通常会区分开发环境、测试环境和生产环境,如果有的项目要求更高的话,可能还会有个预生产环境。
开发环境作为和前端开发联调的环境,一般使用起来都比较随意,而我们在进行本地开发的时候,有时候也会将本地启动的微服务注册到注册中心nacos上,方便进行调试。
这样,注册中心的一个微服务可能就会拥有多个服务实例,就像下面这样:
眼尖的小伙伴肯定发现了,这两个实例的ip地址有一点不同。
线上环境现在一般使用容器化部署,通常都是由流水线工具打成镜像然后扔到docker中运行,因此我们去看一下服务在docker容器内的ip:
可以看到,这就是注册到nacos上的服务地址之一,而列表中192
开头的另一个ip,则是我们本地启动的服务的局域网地址。看一下下面这张图,就能对整个流程一目了然了。
总结一下:
-
两个service都是通过宿主机的ip和port,把自己的信息注册到nacos上
-
线上环境的service注册时使用docker内部ip地址
-
本地的service注册时使用本地局域网地址
那么这时候问题就来了,当我本地再启动一个serviceB,通过FeignClient
来调用serviceA中的接口时,因为Feign本身的负载均衡,就可能把请求负载均衡到两个不同的serviceA实例。
如果这个调用请求被负载均衡到本地serviceA的话,那么没什么问题,两个服务都在同一个192.168
网段内,可以正常访问。但是如果负载均衡请求到运行在docker内的serviceA的话,那么问题来了,因为网络不通,所以会请求失败:
说白了,就是本地的192.168
和docker内的虚拟网段172.17
属于纯二层的两个不同网段,不能互访,所以无法直接调用。
那么,如果想在调试时把请求稳定打到本地服务的话,有一个办法,就是指定在FeignClient
中添加url
参数,指定调用的地址:
@FeignClient(value = "serviceA",url = "http://127.0.0.1:8088/")
public interface ClientA {
@GetMapping("/test/get")
String get();
}
但是这么一来也会带来点问题:
-
代码上线时需要再把注解中的
url
删掉,还要再次修改代码,如果忘了的话会引起线上问题 -
如果测试的
FeignClient
很多的话,每个都需要配置url
,修改起来很麻烦
那么,有什么办法进行改进呢?为了解决这个问题,我们还是得从Feign的原理说起。
Feign原理
Feign的实现和工作原理,我以前写过一篇简单的源码分析,大家可以简单花个几分钟先铺垫一下,Feign核心源码解析。明白了原理,后面理解起来更方便一些。
简单来说,就是项目中加的@EnableFeignClients
这个注解,实现时有一行很重要的代码:
@Import(FeignClientsRegistrar.class)
这个类实现了ImportBeanDefinitionRegistrar
接口,在这个接口的registerBeanDefinitions
方法中,可以手动创建BeanDefinition
并注册,之后spring会根据BeanDefinition
实例化生成bean,并放入容器中。
Feign就是通过这种方式,扫描添加了@FeignClient
注解的接口,然后一步步生成代理对象,具体流程可以看一下下面这张图:
后续在请求时,通过代理对象的FeignInvocationHandler
进行拦截,并根据对应方法进行处理器的分发,完成后续的http请求操作。
ImportBeanDefinitionRegistrar
上面提到的ImportBeanDefinitionRegistrar
,在整个创建FeignClient
的代理过程中非常重要, 所以我们先写一个简单的例子看一下它的用法。先