Spring Cloud Alibaba-Feign的源码分析

1,953 阅读8分钟

一、源码分析

1、 源码推演

我们在想为什么我们调用接口StockFeignClient就能调用对应的服务呢? StockFeignClient接口代码如下:

@FeignClient(name = "msb-stock")//,configuration = StockFeignConfiguration.class)
public interface StockFeignClient {
    /**
     * http://msb-stock/stock/reduce/{productId}
     * @param productId
     * @return
     */
    @GetMapping("/stock/reduce/{productId}")
    String reduce(@PathVariable Integer productId);
}

StockFeignClient接⼝打个@FeignClient注解,它是怎么通过接⼝上的信息、找到接⼝的实现类的呢?我们看一下StockFeignClient发现⾥⾯就是⼀些SpringMVC相关的注解信息,⽐如接⼝类和⽅法上的@RequestMapping中、标注了具体访问时的路径以及请求⽅法(GET、PUT、POST、DELETE)是怎样的、@PathVariable标注了应该在请求路径上带上什么变量名、@RequestBody表示POST请求要带上的请求参数。 还有这个@FeignClient中name属性,这些信息一定是构建一个url, 好再@ReqeustMapping中我们知道我们的路径是/stock/reduce/{productId} 并且是一个get请求,

    @GetMapping("/stock/reduce/{productId}")
    String reduce(@PathVariable Integer productId);

那对应StockFeignClient上的注解@FeignClient注解,可以得到目标服务,也就是本次调用的服务msb-stock

@FeignClient(name = "msb-stock",configuration = StockFeignConfiguration.class)

最终根据这些注解信息得到的请求URL信息为:/msb-stock//stock/reduce/12。

⽽⼜因为在SpringCloud体系内,发送⼀次请求都是通过HTTP协议来的,最终我们加上协议后,请求URL为: http://msb-stock//stock/reduce/45465。

分析到这⾥,我们再看下现在还缺什么:http://msb-stock/stock/reduce/45465,这个请求URL中⽬前唯⼀的疑点就在msb-stock上了,总不能就这么尴尬的写个msb-stock来发送请求吧,没有实际的ip和port怕是直接发起请求就报错了,所以肯定是需要将msb-stock解析成具体的ip和port,这样的URL才算是⼀个完整的URL、才能实际的发送有效请求出去。

并且我们是和nacos进行整合的,那么我们需要从nacos中获取所有服务对应的ip和port ,但是我们如果有多个实例那我们是不是需要利用负载均衡器来获取一个我们需要的服务,当然我们feign也整合了ribbon,所以我们底层可以使用ribbon进行负载均衡。

2、源码入口

源码分析的两种思路,一个是@EnableXXX 作为入口,另一个就是springboot的自动装配 xxxAutoConfiguration

image.png

进⼊到EnableFeignClients注解类中,会发现有个@Import注解,这个注解前面我们经常看到,这里导⼊了⼀个⽐较特别的类:FeignClietnsRegistrar,简单翻译下就是Feign的客户端注册器,注册器?这可能是吧@FeignClient注解标记的那些接口类,进行解析然后注入的

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

3、扫描@FeignClient标注的类

因为他实现了ImportBeanDefinitionRegistrar 所以我们来看他的registerBeanDefinitions方法

public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
    // 解析默认的配置类EnableFeignClients
   registerDefaultConfiguration(metadata, registry);
    // 注册用@FeignClient标注的接口  
   registerFeignClients(metadata, registry);
}

3.1 整体思路分析

image.png

image.png

3.2 获取扫描器

registerFeignClients⽅法⼀进去我们可以看到getScanner⽅法、很明显它就是获取⼀个扫描器,在getScanner⽅法中,发现它new了⼀个ClassPathScanningCandidateComponentProvider类型的对象,⾥⾯有个⽅法isCandidateComponent,来判断是否是我们需要的

protected ClassPathScanningCandidateComponentProvider getScanner() {
   return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
      @Override
      protected boolean isCandidateComponent(
            AnnotatedBeanDefinition beanDefinition) {
         boolean isCandidate = false;
         // 确定基础类是否独立,即它是一个顶级类或嵌套类(静态内部类)可以独立于封闭类构造。
         if (beanDefinition.getMetadata().isIndependent()) {
             // 判断是否是注解
            if (!beanDefinition.getMetadata().isAnnotation()) {
               isCandidate = true;
            }
         }
         return isCandidate;
      }
   };
}

3.3 获取扫描包

image.png

3.4 获取标注@FeignClient的接口并注入容器

获取标注@FeignClient的接口

image.png

image.png

image.png

这里的扫描我们能想起mybatis的扫描。

注入容器

这里设计的类是FeignClientFactoryBean 他是一个FactoryBean 我们获取对象是调用getObject

image.png

总结:

1.以启动类上的@EnableFeignClients为⼊⼝,扫描启动类所在包路径以及该包下所有⼦包中的所有的类

2.从扫描到的类中、筛选出所有打了@FeignClient注解的类

3.解析@FeignClient注解中的属性,创建⼀个BeanDefinition并设置各种属性值,再注⼊到Spring容器中

4、FeignClientFactoryBean创建动态代理

由于FeignClientFactoryBean是tFactoryBean所有获取对象是从getObject中

image.png

image.png

4.1 获取组件属性

从容器中获取对应的FeignContext,

image.png

我们进入feign(contex)

image.png

这些方法都是调用的get方法,只是类型不同而已

image.png

image.png

image.png

image.png

image.png

image.png

在getObject⽅法中,⾸先从applicationContext中、也就是从Spring容器中获取了⼀个FeignContext组件,应该是Feign存放⼯具类的⼀个上下⽂组件,然后从FeignContext中获取到了FeignLoggerFactory组件,⼀路追进去发现,原来在底层也是维护了⼀个Spring容器的缓存Map<String, AnnotationConfigApplicationContext>。Feign在执⾏时涉及到⼀系列的组件,所以Feign⼲脆为每个服务创建了⼀个Spring容器类ApplicationContext,⽤来存放每个服务各⾃所需要的组件,每个服务的这些⼯具类、组件都是互不影,所以我们看到它在底层是通过⼀个Map来缓存的,key为服务名,value为服务所对应的的spring容器ApplicationContext。

接着我们想不管是FeignContext、FeignLoggerFactory还是Encoder、Decoder、Contract,他们都是接⼝,那具体的实现类是什么呢?

因为它们都是直接从Spring的容器中取出来的,这就意味着在某个地⽅事先注⼊到了Spring容器中了,所以我们还得从⼀些地⽅、如配置类⾥⾯看下。

在SpringCloud系列组件中,设计者都⽐较喜欢把⼀些组件的初始化都放在⼀些XxxConfiguration中,然后需要使⽤到的时候通过容器直接取,但是对于我们分析⽽⾔、还是有点麻烦的,毕竟还得去找。

⾸先因为我们当前是在FeignClientFactoryBean中,所以⾸先当然是从它对应路径下开始寻找,看下有没有类似Configuration相关的类,如下图所示:

FeignAutoConfiguration

image.png

FeignClientsConfiguration

image.png

上面是对应get内容

下面对应configureFeign

image.png

我们看方法会发现里面就是先获取java类配置,然后再获取属性配置,属性配置会把代码配置给覆盖掉,所以我们需要属性配置优先级高于代码

image.png

4.2 创建动态代理对象

因为@FeignClient默认属性是name,所以url为空,所以第一个if条件成立;而注解中name属性值是msb-stock,所以url最后为http://msb-stock然后进⼊loadBalance⽅法中,从⽅法名称上来看、从这⾥开始要和负载均衡有点关系了。

image.png

获取的Client是LoadBalanceFeignClient,对应的Targeter是HystrixTargeter

image.png

创建实例

image.png

创建动态代理的类。

image.png

在newInstance⽅法中,⼀开始先调⽤targetToHandlersByName获取了Map<String, MethodHandler>,因为我们这⾥是动态代理、⽽且代理的是接⼝的⽅法,所以我们基本可以猜测到这⾥是想要为每个⽅法创建⼀个⽅法的处理器,也就是MethodHandler,但是具体怎么创建呢?

image.png

我们进入factory.create查看是怎样创建MethodHandler

这里重点就是我们会把我们对应的接口封装为<font color="red">SynchronousMethodHandler</font>,后面调用的时候我们会进入这个方法

image.png

理解了前⾯的内容之后,现在就⽐较好理解了,创建动态代理、怎么代理呢,就是遍历了下⽅法,把之前的Map<String, MethodHandler>、本来以⽅法名称为key的形式、换成了以Method为key的methodToHandler的Map<Method, MethodHandler>。

⽅便通过Method就能从Map获取到对应的Handler,然后创建了InvocationHandler,通过JDK的API 创建⼀个JDK的动态代理并返回。

好,我们查看创建jdk动态代理InvocationHandler

image.png

=image.png

到这⾥我们基本上可以知道:⽐如当服务B的getById⽅法被调⽤时、会调⽤服务A的getById⽅法,⽽服务A这⾥因为是通过Feign声明式调⽤的,肯定会先通过服务A的Feign动态代理FeignInvocationHandler,从动态代理中的Map<Method, MethodHandler>中、找到具体某个⽅法的SynchronousMethodHandler,然后执⾏该SynchronousMethodHandler中的invoke⽅法来处理⽅法的逻辑。

并且根据我们最早的猜测,SynchronousMethodHandler中的逻辑⽆⾮就是将前⾯从@FeignClient、接⼝上的@RequestMapping、@PathVariable、@RequestParams等注解中解析到的信息,拼凑成⼀个完整的HTTP请求,然后发起请求并接对应的响应信息。

4.3 进行调用

jdk动态代理的调用,会调用FeignInvocationHandler.invoke

从缓存中通过Method获取对应的MethodHandler (SynchronousMethodHandler

image.png

image.png

然后调用SynchronousMethodHandler

image.png

这里通过client进行执行,这里面应该有负载均衡和调用

image.png

image.png

4.3.1 获取负载均衡器

image.png

image.png

image.png

4.3.2通过负载均衡器获取服务

image.png

LoadBalancerCommand从名称可以推测这是一个负载均衡器,调用submit里面会调用selectServer方法

image.png

image.png

image.png

获取负载均衡器ZoneAwareLoadBalancer,然后根据路由规则进行调用这里就是ribbon的内容了

image.png

4.3.3 重构URL

image.png

可以进入看一下就是拼接字符串

image.png

4.3.4 发送http请求

image.png

image.png

默认我们会调用最后HttpUrlConnection

image.png

image.png

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第8天,点击查看活动详情