文章目录
- 一、前言
- 二、@FeignClient解析
-
- 1、@FeignClient注解解释
- 2、@FeignClient注解作用
-
- 1)使用@RibbonClient自定义负载均衡策略
- 三、@EnableFeignClients解析
-
- 1、FeignClientsRegistrar类
- 2、注册默认配置
- 3、注册所有的FeignClient流程图
- 4、注册所有的FeignClient
-
- 1)获取包扫描路径
- 2)扫描所有的FeignClient
- 3)注册FeignClient
- 四、后续文章
一、前言
在前面的文章:
- 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.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.6.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--整合spring cloud alibaba-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</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、@FeignClient注解解释
@FeignClient注解中定义了一些方法,如下:
1> value()和name()互为别名
- 表示微服务名;
2> serviceId()
- 已经废弃了,直接使用name即可;
3> contextId()
- 存在多个相同名称FeignClient时,可以使用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()
- 用于配置指定服务的地址 / IP,相当于直接请求这个服务,不经过Ribbon的负载均衡。
6> decode404()
- 当调用请求发生404错误时,如果decode404的值为true,会执行decoder解码用404代替抛出FeignException异常,否则直接抛出异常。
7> configuration()
- OpenFeign的配置类,在配置类中可以自定义Feign的Encoder、Decoder、LogLevel、Contract等。
8> fallback()
- 定义容错的处理类(回退逻辑),fallback类必须实现FeignClient的接口。
9> fallbackFactory()
- 也是容错的处理,但是可以知道熔断的异常信息。
10> path()
- path定义当前FeignClient访问接口时的统一前缀,比如接口地址是/user/get, 如果你定义了前缀是user, 那么具体方法上的路径就只需要写/get 即可。
2、@FeignClient注解作用
用@FeignClient注解标注一个接口后,OpenFeign会对这个接口创建一个对应的动态代理 --> REST client(发送restful请求的客户端),然后可以将这个REST client注入其他的组件(比如ServiceBController);如果启用了ribbon,就会采用负载均衡的方式,来进行http请求的发送。
1)使用@RibbonClient自定义负载均衡策略
可以用@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解析
我们知道@EnableFeignClients
注解用于开启OpenFeign,可以大胆猜测,@EnableFeignClients注解 会触发OpenFeign的核心机制:去扫描所有包下面的@FeignClient注解的接口、生成@FeignClient标注接口的动态代理类。
下面我们就基于这两个猜测解析@EnableFeignClients。
@EnableFeignClients
注解中通过@Import
导入了一个FeignClientsRegistrar
类,FeignClientsRegistrar负责FeignClient的注册(即:扫描指定包下的@FeignClient注解标注的接口、生成FeignClient动态代理类、触发后面的其他流程)。
1、FeignClientsRegistrar类
由于FeignClientsRegistrar
实现自ImportBeanDefinitionRegistrar
,结合我们在SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)一文对OpenFeign入口的分析,得知,在SpringBoot启动过程中会进入到FeignClientsRegistrar#registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry)
方法;
registerBeanDefinitions()
方法是feign的核心入口方法,其中会做两件事:注册默认的配置、注册所有的FeignClient。下面我们分开来看;
2、注册默认配置
registerDefaultConfiguration()
方法负责注册OpenFeign的默认配置。具体的代码执行流程如下:
方法流程解析:
- 首先获取
@EnableFeignClients
注解的全部属性;- 如果属性不为空,并且属性中包含defaultConfiguration,则默认字符串
default.
和 启动类全路径名拼接到一起;
- 然后再拼接上
.FeignClientSpecification
,作为beanName,构建出一个BeanDefinition,将其注册到BeeanDefinictionRegistry中。
注册默认配置流程很简单清晰,复杂的在于注册所有的FeignClient,下面我就继续来看。
3、注册所有的FeignClient流程图
4、注册所有的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?
1)获取包扫描路径
FeignClientsRegistrar#getBasePackages(metadata)
方法负责获取包路径;
方法执行逻辑解析:
- 首先获取@EnableFeignClients注解中的全部属性;
- 如果指定了
basePackages
,则采用basePackages指定的目录作为包扫描路径;- 如果指定了一些
basePackageClasses
,则采用basePackageClasses指定的类们所在的目录 作为包扫描路径;- 如果既没有指定
basePackages
,也没有指定basePackageClasses,则采用启动类所在的目录作为包扫描路径。默认是这种情况。
2)扫描所有的FeignClient
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的扫描也就结束了;
3)注册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如何负载均衡?
转载请注明:【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient | 胖虎的工具箱-编程导航