【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

2年前 (2022) 程序员胖胖胖虎阿
164 0 0

文章目录

  • 一、前言
  • 二、@FeignClient解析
    • 1、@FeignClient注解解释
    • 2、@FeignClient注解作用
      • 1)使用@RibbonClient自定义负载均衡策略
  • 三、@EnableFeignClients解析
    • 1、FeignClientsRegistrar类
    • 2、注册默认配置
    • 3、注册所有的FeignClient流程图
    • 4、注册所有的FeignClient
      • 1)获取包扫描路径
      • 2)扫描所有的FeignClient
      • 3)注册FeignClient
  • 四、后续文章

一、前言

在前面的文章:

  1. SpringCloud之Feign实现声明式客户端负载均衡详细案例
  2. SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)
  3. SpringCloud之OpenFeign的常用配置(超时、数据压缩、日志)
  4. SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
  5. SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)

我们聊了以下内容:

  1. OpenFeign的概述、为什么会使用Feign代替Ribbon?
  2. Feign和OpenFeign的区别?
  3. 详细的OpenFeign实现声明式客户端负载均衡案例
  4. OpenFeign中拦截器RequestInterceptor的使用
  5. OpenFeign的一些常用配置(超时、数据压缩、日志输出)
  6. SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
  7. 在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,其中:

  1. @EnableFeignClients,用来开启OpenFeign;
  2. @FeignClient,标记要用OpenFeign来拦截的请求接口;

结合之前之前的博文(SpringCloud之Feign实现声明式客户端负载均衡详细案例):
【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

为什么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。

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

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

1、FeignClientsRegistrar类

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

由于FeignClientsRegistrar实现自ImportBeanDefinitionRegistrar,结合我们在SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)一文对OpenFeign入口的分析,得知,在SpringBoot启动过程中会进入到FeignClientsRegistrar#registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry)方法;
【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient
registerBeanDefinitions()方法是feign的核心入口方法,其中会做两件事:注册默认的配置、注册所有的FeignClient。下面我们分开来看;

2、注册默认配置

registerDefaultConfiguration()方法负责注册OpenFeign的默认配置。具体的代码执行流程如下:
【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient
方法流程解析:

  1. 首先获取@EnableFeignClients注解的全部属性;
  2. 如果属性不为空,并且属性中包含defaultConfiguration,则默认字符串default. 和 启动类全路径名拼接到一起;
    【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient
  3. 然后再拼接上.FeignClientSpecification,作为beanName,构建出一个BeanDefinition,将其注册到BeeanDefinictionRegistry中。

注册默认配置流程很简单清晰,复杂的在于注册所有的FeignClient,下面我就继续来看。

3、注册所有的FeignClient流程图

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

4、注册所有的FeignClient

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

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient
【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

方法逻辑解析:

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

下面,我们细看一下如何获取包扫描路径?如何扫描到FeignClient?如何注册FeignClient?

1)获取包扫描路径

FeignClientsRegistrar#getBasePackages(metadata)方法负责获取包路径;
【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

方法执行逻辑解析:

  1. 首先获取@EnableFeignClients注解中的全部属性;
  2. 如果指定了basePackages,则采用basePackages指定的目录作为包扫描路径;
  3. 如果指定了一些basePackageClasses,则采用basePackageClasses指定的类们所在的目录 作为包扫描路径;
  4. 如果既没有指定basePackages,也没有指定basePackageClasses,则采用启动类所在的目录作为包扫描路径。默认是这种情况。

2)扫描所有的FeignClient

ClassPathScanningCandidateComponentProvider#findCandidateComponents(String basePackage)方法负责扫描出指定目录下的所有标注了@FeignClient注解的Class类(包括interface、正常的Class)。

具体代码执行流程如下:

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

方法逻辑解析:

  1. 首先扫描出指定路径下的所有Class文件;
  2. 接着遍历每个Class文件,使用Scanner中的@FeignClient过滤器过滤出所有被@FeignClient注解标注的Class;
  3. 最后将过滤出的所有Class返回。

细看一下isCandidateComponent(MetadataReader metadataReader)方法:

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

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

【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient
到这里,FeignClient的扫描也就结束了;

3)注册FeignClient

扫描到所有的FeignClient之后,需要将其注入到Spring中,FeignClientsRegistrar#registerFeignClient()方法负责这个操作;
【云原生&微服务十四】SpringCloud之深度源码剖析OpenFeign如何扫描所有的FeignClient

注册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如何负载均衡?

相关文章

暂无评论

暂无评论...