在前面的文章:
我们聊了以下内容:
- OpenFeign的概述、为什么会使用Feign代替Ribbon
- Feign和OpenFeign的区别
- 详细的OpenFeign实现声明式客户端负载均衡案例
- OpenFeign中拦截器RequestInterceptor的使用
- OpenFeign的一些常用配置(超时、数据压缩、日志输出)
- SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
- 在SpringBoot启动流程中开启OpenFeign的入口
本文基于OpenFeign低版本(SpringCloud 2020.0.x版本之前)讨论:@FeignClient注解在哪里被扫描?
PS:本文基于的SpringCloud版本
2.3.7.RELEASE
Hoxton.SR9
2.2.6.RELEASE
org.springframework.boot
spring-boot-dependencies
${spring-boot.version}
pom
import
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
${spring-cloud-alibaba.version}
pom
import
后续分析完Feign的低版本实现,博主会再出一版OpenFeign新版本的系列文章。
我们知道OpenFeign有两个注解:@EnableFeignClients 和 @FeignClient,其中:
- @EnableFeignClients,用来开启OpenFeign;
- @FeignClient,标记要用OpenFeign来拦截的请求接口;
结合之前之前的博文(SpringCloud之Feign实现声明式客户端负载均衡详细案例):

为什么Service-B服务中定义了一个ServiceAClient接口(继承自ServiceA的API接口),某Controller 或Service中通过@Autowried注入一个ServiceAClient接口的实例,就可以通过OpenFeign做负载均衡去调用ServiceA服务?
先看@FeignClient注解
@FeignClient注解中定义了一些方法,如下:
1> value()和name()互为别名
2> serviceId()
3> contextId()
4> qualifier()
对应Spring的@Qualifier注解,在定义@FeignClient时,指定qualifier;
在@Autowired注入FeignClient时,使用@Qualifier注解;
// FeignClient定义
@FeignClient(name = "SERVICE-A", contextId = "9999", qualifier = "serviceAClient1")
public interface ServiceAClient extends ServiceA {
}
// FeignClient注入
@Autowired
@Qualifier("serviceAClient1")
private ServiceAClient serviceAClient;
5> url()
6> decode404()
7> configuration()
8> fallback()
9> fallbackFactory()
10> path()
用@FeignClient注解标注一个接口后,OpenFeign会对这个接口创建一个对应的动态代理 --> REST client(发送restful请求的客户端),然后可以将这个REST client注入其他的组件(比如ServiceBController);如果启用了ribbon,就会采用负载均衡的方式,来进行http请求的发送。
可以用@RibbonClient标注一个配置类,在@RibbonClient注解的configuration属性中可以指定配置类,自定义自己的ribbon的ILoadBalancer;@RibbonClient的名称,要跟@FeignClient的名称一样。
<1> 在SpringBoot扫描不到的目录下新建一个配置类:
@Configuration
public class MyConfiguration {
@Bean
public IRule getRule() {
return new MyRule();
}
@Bean
public IPing getPing() {
return new MyPing();
}
}
<2> 在SpringBoot可以扫描到的目录下新建一个配置类(被@RibbonClient注解标注):
由于@FeignClient中填的name() / value()是SERVICE-A,所以@RibbonClient的value() 也必须是SERVICE-A,表示针对调用服务SERVICE-A时做负载均衡。
@Cinfiguration
@RibbonClient(name = “SERVICE-A”, configuration = MyConfiguration.class)
public class ServiceAConfiguration {
}
我们知道@EnableFeignClients注解用于开启OpenFeign,可以大胆猜测,@EnableFeignClients注解 会触发OpenFeign的核心机制:去扫描所有包下面的@FeignClient注解的接口、生成@FeignClient标注接口的动态代理类。
下面我们就基于这两个猜测解析@EnableFeignClients。

@EnableFeignClients注解中通过@Import导入了一个FeignClientsRegistrar类,FeignClientsRegistrar负责FeignClient的注册(即:扫描指定包下的@FeignClient注解标注的接口、生成FeignClient动态代理类、触发后面的其他流程)。

由于FeignClientsRegistrar实现自ImportBeanDefinitionRegistrar,结合我们在SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)一文对OpenFeign入口的分析,得知,在SpringBoot启动过程中会进入到FeignClientsRegistrar#registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry)方法;

registerBeanDefinitions()方法是feign的核心入口方法,其中会做两件事:注册默认的配置、注册所有的FeignClient。下面我们分开来看;
registerDefaultConfiguration()方法负责注册OpenFeign的默认配置。具体的代码执行流程如下:

方法流程解析:
- 首先获取
@EnableFeignClients注解的全部属性;- 如果属性不为空,并且属性中包含defaultConfiguration,则默认字符串
default.和 启动类全路径名拼接到一起;
- 然后再拼接上
.FeignClientSpecification,作为beanName,构建出一个BeanDefinition,将其注册到BeeanDefinictionRegistry中。
注册默认配置流程很简单清晰,复杂的在于注册所有的FeignClient,下面我就继续来看。

registerFeignClients()方法负责注册所有的FeignClient;


方法逻辑解析:
- 首先获取@EnableFeignClients注解的所有属性,主要为了拿到扫描包路径(basePackages);
- 因为一般不会在@EnableFeignClients注解中配置clients属性,所以会进入到clients属性为空时的逻辑;
- 然后通过
getScanner()方法获取扫描器:ClassPathScanningCandidateComponentProvider,并将上下文AnnotationConfigServletWebServerApplicationContext作为扫描器的ResourceLoader;- 接着给扫描器
ClassPathScanningCandidateComponentProvider添加一个注解过滤器(AnnotationTypeFilter),只过滤出包含@FeignClient注解的BeanDefinition;- 再通过
getBasePackages(metadata)方法获取@EnableFeingClients注解中的指定的包扫描路径 或 扫描类;如果没有获取到,则默认扫描启动类所在的包路径;- 然后进入到核心逻辑:通过
scanner.findCandidateComponents(basePackage)方法从包路径下扫描出所有标注了@FeignClient注解并符合条件装配的接口;- 最后将FeignClientConfiguration 在BeanDefinitionRegistry中注册一下,再对FeignClient做真正的注册操作。
下面,我们细看一下如何获取包扫描路径?如何扫描到FeignClient?如何注册FeignClient?
FeignClientsRegistrar#getBasePackages(metadata)方法负责获取包路径;

方法执行逻辑解析:
- 首先获取@EnableFeignClients注解中的全部属性;
- 如果指定了
basePackages,则采用basePackages指定的目录作为包扫描路径;- 如果指定了一些
basePackageClasses,则采用basePackageClasses指定的类们所在的目录 作为包扫描路径;- 如果既没有指定
basePackages,也没有指定basePackageClasses,则采用启动类所在的目录作为包扫描路径。默认是这种情况。
ClassPathScanningCandidateComponentProvider#findCandidateComponents(String basePackage)方法负责扫描出指定目录下的所有标注了@FeignClient注解的Class类(包括interface、正常的Class)。
具体代码执行流程如下:

方法逻辑解析:
- 首先扫描出指定路径下的所有Class文件;
- 接着遍历每个Class文件,使用Scanner中的@FeignClient过滤器过滤出所有被@FeignClient注解标注的Class;
- 最后将过滤出的所有Class返回。
细看一下isCandidateComponent(MetadataReader metadataReader)方法:

其中会遍历Scanner中的所有excludeFilters和includeFilters对当前Class做过滤操作,就此处,仅有一个includeFilter,用来过滤出标注了@FeignClient注解的Class,具体的过滤逻辑如下:

到这里,FeignClient的扫描也就结束了;
扫描到所有的FeignClient之后,需要将其注入到Spring中,FeignClientsRegistrar#registerFeignClient()方法负责这个操作;

注册FeignClient实际就是构建一个FeignClient对应的BeanDefinition,然后将FeignClient的一些属性配置设置为BeanDefinition的property,最后将BeanDefinition注册到Spring的临时容器。在处理FeignClient的属性配置时,如果@FeignClient中配置了qualifier,则使用qualifier作为beanName。
到这里已经完成了包的扫描、FeignClient的解析、FeignClient数据以BeanDefinition的形式存储到spring框架中的BeanDefinitionRegistry中。
下面需要去创建实现标注了@FeignClient注解的ServiceAClient接口的动态代理,将动态代理作为一个bean,注入给调用方(ServiceBControler);这个我们放在下一篇文章聊,敬请期待。
OpenFeign如何生成FeignClient的动态代理类?OpenFeign如何负载均衡?
先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦