在前面的文章:
- SpringCloud之Feign实现声明式客户端负载均衡详细案例
- SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
- SpringCloud之OpenFeign的常用配置(超时、数据压缩、日志)
- SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
- SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)
我们聊了以下内容:
- OpenFeign的概述、为什么会使用Feign代替Ribbon?
- Feign和OpenFeign的区别?
- 详细的OpenFeign实现声明式客户端负载均衡案例
- OpenFeign中拦截器RequestInterceptor的使用
- OpenFeign的一些常用配置(超时、数据压缩、日志输出)
- SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
- 在SpringBoot启动流程中开启OpenFeign的入口
本文基于OpenFeign低版本(SpringCloud 2020.0.x版本之前
)讨论:@FeignClient注解在哪里被扫描?
PS:本文基于的SpringCloud版本
<properties>
<spring-boot.version>2.3.7.RELEASEspring-boot.version>
<spring-cloud.version>Hoxton.SR9spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASEspring-cloud-alibaba.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>${spring-cloud.version}version>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>${spring-cloud-alibaba.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
后续分析完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()
@Qualifier
注解,在定义@FeignClient时,指定qualifier;@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注解标注):
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的默认配置。具体的代码执行流程如下:
方法流程解析:
注册默认配置流程很简单清晰,复杂的在于注册所有的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之后,需要将其注入到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如何负载均衡?