• 【云原生&微服务十三】SpringCloud之深度源码剖析OpenFeign如何扫描所有的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版本

     
        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
            
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    后续分析完Feign的低版本实现,博主会再出一版OpenFeign新版本的系列文章。

    我们知道OpenFeign有两个注解:@EnableFeignClients@FeignClient,其中:

    1. @EnableFeignClients,用来开启OpenFeign;
    2. @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;
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9

    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();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    <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的默认配置。具体的代码执行流程如下:
    在这里插入图片描述
    方法流程解析:

    1. 首先获取@EnableFeignClients注解的全部属性;
    2. 如果属性不为空,并且属性中包含defaultConfiguration,则默认字符串default. 和 启动类全路径名拼接到一起;
      在这里插入图片描述
    3. 然后再拼接上.FeignClientSpecification,作为beanName,构建出一个BeanDefinition,将其注册到BeeanDefinictionRegistry中。

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

    3、注册所有的FeignClient流程图

    在这里插入图片描述

    4、注册所有的FeignClient

    registerFeignClients()方法负责注册所有的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)方法负责获取包路径;
    在这里插入图片描述

    方法执行逻辑解析:

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

    2)扫描所有的FeignClient

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

    具体代码执行流程如下:

    在这里插入图片描述

    方法逻辑解析:

    1. 首先扫描出指定路径下的所有Class文件;
    2. 接着遍历每个Class文件,使用Scanner中的@FeignClient过滤器过滤出所有被@FeignClient注解标注的Class;
    3. 最后将过滤出的所有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如何负载均衡?

    先自我介绍一下,小编13年上师交大毕业,曾经在小公司待过,去过华为OPPO等大厂,18年进入阿里,直到现在。深知大多数初中级java工程师,想要升技能,往往是需要自己摸索成长或是报班学习,但对于培训机构动则近万元的学费,着实压力不小。自己不成体系的自学效率很低又漫长,而且容易碰到天花板技术停止不前。因此我收集了一份《java开发全套学习资料》送给大家,初衷也很简单,就是希望帮助到想自学又不知道该从何学起的朋友,同时减轻大家的负担。添加下方名片,即可获取全套学习资料哦

  • 相关阅读:
    LeetCode 子串 子数组 系列题目总结
    ASPICE系列:顺利通过ASPICE流程软件单元验证(SWE.4)
    多线程学习笔记-3.并发容器
    Three.js教程之在网页快速实现 3D效果(教程含源码)
    Nginx超时配置
    RK3399平台开发系列讲解(电源管理篇)11.11、调节器消费者接口
    Java网络编程
    解读凯文·凯利《失控》
    【排序算法】堆排序详解与实现
    代码随想录 Day49 单调栈01 LeetCode LeetCodeT739每日温度 T496 下一个最大元素I
  • 原文地址:https://blog.csdn.net/m0_67392273/article/details/126080720