当前位置: 首页 > article >正文

Spring Cloud源码 - Feign源码原理分析

Feign原理

文章目录

  • Feign原理
    • 一:理解远程调用
    • 二:Feign和OpenFeign
    • 三:OpenFeign核心流程
      • 1:包扫描流程
      • 2:注册FeignClient到Spring
      • 3:动态代理原理
      • 4:解析MVC注解的原理
      • 5:发送请求原理
      • 6:feign和ribbon的整合
      • 7:处理响应的原理

一:理解远程调用

远程调用本地调用是相对的

本地调用就是同一个 Service 里面的方法 A 调用方法 B。

那远程调用就是不同 Service 之间的方法调用。

Service 级的方法调用,我们自己构造请求 URL和请求参数,就可以发起远程调用了。

在服务之间调用的话,我们都是基于 HTTP 协议

一般用到的远程服务框架有 OKHttp3,Netty, HttpURLConnection 等

在这里插入图片描述
但是这种虚线方框中的构造请求的过程是很繁琐的,有没有更简便的方式呢?-> feign

如果让你设计一个远程调用的组件,你会考虑什么问题?

  • 如何使远程调用像本地方法调用简单?(包扫描+动态代理)
  • Feign 如何找到远程服务的地址的?
  • Feign 是如何进行负载均衡的?(Feign 如何和 Ribbon 一起工作的)

二:Feign和OpenFeign

OpenFeign 组件的前身是 Netflix Feign 项目,它最早是作为 Netflix OSS 项目的一部分,由 Netflix 公司开发

后来 Feign 项目被贡献给了开源组织,于是才有了我们今天使用的 Spring Cloud OpenFeign 组件

Feign 和 OpenFeign 有很多大同小异之处,不同的是 OpenFeign 支持 MVC 注解

可以简单的理解为OpenFeign是Feign的加强版本

  1. OpenFeign 是声明式的 HTTP 客户端,让远程调用更简单
  2. 提供了HTTP请求的模板,编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息
  3. 整合了Ribbon(负载均衡组件)和 Hystix(服务熔断组件),不需要显示使用这两个组件
  4. Spring Cloud Feign 在 Netflix Feign的基础上扩展了对SpringMVC注解的支持

三:OpenFeign核心流程

在这里插入图片描述

  1. 在服务的启动阶段,服务A的OpenFeign框架会发起一个主动扫描包的流程
  2. 从指定目录下扫描并加载所有的被@FeignClient注解修饰的接口,然后转换成为Bean
  3. 这些接口会经过MVC Contract解析,将注解解析出来,放到元数据中
  4. 基于上面加载的每一个 FeignClient 接口,会生成一个动态代理对象,指向了一个包含对应方法的 MethodHandler 的 HashMap。MethodHandler 对元数据有引用关系。生成的动态代理对象会被添加到 Spring 容器中,并注入到对应的服务里
  5. 服务A进行接口调用,准备发起远程调用
  6. 从动态代理对象Proxy中找到一个MethodHandler实例,生成request,包含有服务的请求URL
  7. 经过负载均衡算法找到一个服务的IP地址,拼接出请求的URL
  8. 服务B处理服务A发起的请求,执行业务逻辑之后,返回相应给服务A

1:包扫描流程

涉及到了一个 OpenFeign 的注解:@EnableFeignClients -> 开启 OpenFeign 功能的

在这里插入图片描述

// 启动类加上这个注解
@EnableFeignClients(basePackages = "com.jackson0714.passjava.member.feign")


// EnableFeignClients 类还引入了 FeignClientsRegistrar 类
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
  ...
}

FeignClientsRegistrar 负责 Feign 接口的加载

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   // 注册配置
   registerDefaultConfiguration(metadata, registry);
   // 注册 FeignClient
   registerFeignClients(metadata, registry);
}

调用 find 方法来查找指定路径 basePackage 的所有带有 @FeignClients 注解的带有 @FeignClient 注解的类、接口

Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);

// 然后只保留接口
// 判断是否是带有注解的 Bean。
if (candidateComponent instanceof AnnotatedBeanDefinition) {
  // 判断是否是接口
   AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
   AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
  // @FeignClient 只能指定在接口上。
   Assert.isTrue(annotationMetadata.isInterface(),
         "@FeignClient can only be specified on an interface");
}

2:注册FeignClient到Spring

在 registerFeignClients 方法中,当 FeignClient 扫描完后,就要为这些 FeignClient 接口生成一个动态代理对象,可以看到下面的代码片段

BeanDefinitionBuilder definition = BeanDefinitionBuilder
      .genericBeanDefinition(FeignClientFactoryBean.class);

核心就是 FeignClientFactoryBean 类,根据类的名字我们可以知道这是一个工厂类,用来创建 FeignClient Bean 的

在使用@FeignClient注解的时候,会指定一些属性,最常见的就是远程服务的服务名称

当OpenFeign 框架去创建 FeignClient Bean 的时候,就会使用这些参数去生成 Bean

在这里插入图片描述

  1. 解析@FeignClient中的属性
  2. 将注解@FeignClient的属性 + 接口的信息构成一个beanDefinition(Spring源码中)
  3. 然后将beanDefinition转换成为一个holder(包含了各种信息)
  4. 最后将holder注册到Spring容器中
// 生成 beanDefinition
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
// 转换成 holder,包含了 beanDefinition, alias, beanName 信息
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
// 注册到 Spring 上下文中。
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

远程的类已经成为自己Spring容器中的bean了,所以后续调用就可以向调用本地方法一样了:

@Autowired
private StudyTimeFeignService studyTimeFeignService; // 像调用本地方法一样

// 省略部分代码
// 直接调用 
studyTimeFeignService.getMemberStudyTimeListTest(id);

3:动态代理原理

在创建 FeignClient Bean 的过程中就会去生成动态代理对象。

调用接口时,其实就是调用动态代理对象的方法来发起请求的。

分析动态代理的入口方法为 getObject()。源码如下所示:

Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
      new HardCodedTarget<>(this.type, this.name, url));

接着调用 target 方法这一块,里面的代码真的很多很细,我把核心的代码拿出来给大家讲下,这个 target 会有两种实现类:

DefaultTargeter 和 HystrixTargeter。

而不论是哪种 target,都需要去调用 Feign.java 的 builder 方法去构造一个 feign client。

在构造的过程中,依赖 ReflectiveFeign 去构造。源码如下:

// 省略部分代码
public class ReflectiveFeign extends Feign {
  	// 为 feign client 接口中的每个接口方法创建一个 methodHandler
	public <T> T newInstance(Target<T> target) {
    for(...) {
        methodToHandler.put(method, handler);
    }
    // 基于 JDK 动态代理的机制,创建了一个 passjava-study 接口的动态代理,所有对接口的调用都会被拦截,然后转交给 handler 的方法。
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
          new Class<?>[] {target.type()}, handler);
}

ReflectiveFeign 做的工作就是为带有 @FeignClient 注解的接口,创建出接口方法的动态代理对象。

@FeignClient("passjava-study")
public interface StudyTimeFeignService {
    @RequestMapping("study/studytime/member/list/test/{id}")
    public R getMemberStudyTimeList(@PathVariable("id") Long id);
}

在这里插入图片描述

解析 FeignClient 接口上各个方法级别的注解,比如远程接口的 URL、接口类型(Get、Post 等)、各个请求参数等。

这里用到了 MVC Contract 协议解析,后面会讲到。

然后将解析到的数据封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理。

相当于把服务的请求地址、接口类型等都帮我们封装好了。这些 MethodHandler 方法会放到一个 HashMap 中。

然后会生成一个 InvocationHandler 用来管理这个 hashMap,其中 Dispatch 指向这个 HashMap。

然后使用 Java 的 JDK 原生的动态代理,实现了 FeignClient 接口的动态代理 Proxy 对象。

这个 Proxy 会添加到 Spring 容器中。

当要调用接口方法时,其实会调用动态代理 Proxy 对象的 methodHandler 来发送请求

在这里插入图片描述

4:解析MVC注解的原理

上面我们讲到了接口上是有一些注解的,比如 @RequestMapping,@PathVariable,这些注解统称为 Spring MVC 注解。

但是由于 OpenFeign 是不理解这些注解的,所以需要进行一次解析

在这里插入图片描述
而解析的类就是 SpringMvcContract 类,调用 parseAndValidateMetadata 进行解析。

解析完之后,就会生成元数据列表。源码如下所示:

List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
  • 方法的定义,如 StudyTimeFeignService 的 getMemberStudyTimeList() 方法。
  • 方法的参数类型,如 Long。
  • 发送 HTTP 请求的地址,如 /study/studytime/member/list/test/{id}

然后每个接口方法就会有对应的一个 MethodHandler,它里面就包含了元数据

当我们调用接口方法时,其实是调用动态代理对象的 MethodHandler 来发送远程调用请求的

在这里插入图片描述
上面我们针对 OpenFeign 框架如何为 FeignClient 接口生成动态代理已经讲完了,下面我们再来看下当我们调用接口方法时,动态代理对象是如何发送远程调用请求的

5:发送请求原理

在这里插入图片描述
还是在 ReflectiveFeign 类中,有一个 invoke 方法,会执行以下代码:

dispatch.get(method).invoke(args);

dispatch指向了一个 HashMap,里面包含了 FeignClient 每个接口的 MethodHandler 类。

这行代码的意思就是根据 method 找到 MethodHandler,调用它的 invoke 方法,且传的参数就是我们接口中的定义的参数。

public Object invoke(Object[] argv) throws Throwable {
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  ...
}

我们可以看到这个方法里面生成了 RequestTemplate,它的值类似如下:

GET /study/list/test/1 HTTP/1.1

RequestTemplate 转换成 Request,它的值类似如下:

GET http://passjava-study/study/list/test/1 HTTP/1.1

这不路径不就是我们要 study 服务的方法,这样就可以直接调用到 study 服了呀!

接着 MethodHandler 会执行以下方法,发起 HTTP 请求。

client.execute(request, options);

但是这个服务的具体 IP 地址我们是不知道的,那 OpenFeign 是如何获取到 服务的 IP 地址的呢?

回想下最开始我们提出的核心问题:OpenFeign 是如何进行负载均衡的 - ribbon

6:feign和ribbon的整合

在这里插入图片描述
将服务名称从 Request 的 URL 中删掉,剩下的如下所示:

GET http:///study/list/test/1 HTTP/1.1

根据服务名从缓存中找 FeignLoadBalancer,如果缓存中没有,则创建一个 FeignLoadBalancer。

FeignLoadBalancer 会创建出一个 command,这个 command 会执行一个 sumbit 方法。

submit 方法里面就会用 Ribbon 的负载均衡算法选择一个 server。源码如下:

Server svc = lb.chooseServer(loadBalancerKey);

通过 debug 调试,我们可以看到两次请求的端口号不一样,一个是 12200,一个是 12100,说明确实进行了负载均衡。

在这里插入图片描述

  • 然后将 IP 地址和之前剔除服务名称的 URL 进行拼接,生成最后的服务地址。
  • 最后 FeignLoadBalancer 执行 execute 方法发送请求。

7:处理响应的原理

当远程服务处理完业务逻辑后,就会返回 reponse 给服务了,这里还会对 reponse 进行一次解码操作。

Object result = decode(response);

这个里面做的事情就是调用 ResponseEntityDecoder 的 decode 方法,将 Json 字符串转化为 Bean 对象。


http://www.kler.cn/a/563403.html

相关文章:

  • AI工具箱最新使用教程
  • vue3中怎么给图片打红框进行标注
  • Docker 搭建 MySQL 数据库
  • Spring 创建对象的流程
  • apache-maven-3.2.1
  • LeetCode热题100- 最小栈【JavaScript讲解】
  • RMII(Reduced Media Independent Interface)详解
  • Java Junit框架
  • libwebsockets交叉编译全流程
  • go-基础笔记
  • 基于SpringBoot+mybatisplus+vueJS企业数据保护系统设计与实现
  • 关于在java项目部署过程MySQL拒绝连接的分析和解决方法
  • MongoDB#常用语句
  • OpenCV开源机器视觉软件
  • 计算机毕业设计SpringBoot+Vue.js车辆管理系统(源码+文档+PPT+讲解)
  • AGI的三重架构假说:模块化认知系统设计——通向通用人工智能的工程化路径
  • 阿里重磅模型深夜开源;DeepSeek宣布开源DeepGEMM;微软开源多模态AI Agent基础模型Magma...|网易数智日报
  • HTTP与网络安全
  • 阻抗和电阻
  • 浅析 Redis 分片集群 Cluster 原理、手动搭建、动态伸缩集群、故障转移