0
点赞
收藏
分享

微信扫一扫

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程


文章目录

  • ​​一、前言​​
  • ​​二、OpenFeign处理HTTP请求​​
  • ​​0、整体处理请求流程图​​
  • ​​1、动态代理处理请求的入口​​
  • ​​2、SynchronousMethodHandler处理请求机制​​
  • ​​1)创建请求模板(SpringMvcContract解析方法参数)​​
  • ​​2)执行请求并解码返回值​​
  • ​​1> 应用所有的RequestInterceptor​​
  • ​​2> LoadBalancerFeignClient负载均衡执行请求流程图​​
  • ​​2> LoadBalancerFeignClient负载均衡执行请求详述​​
  • ​​<1> Feign是如何和Ribbon、Eureka整合在一起的?FeignLoadBalancer中用了Ribbon的那个ILoadBalancer?​​
  • ​​<2> Feign如何使用Ribbon进行负载均衡?​​
  • ​​<3> 最终发送出去的请求URI是什么样的?​​
  • ​​3> 指定Decoder时对请求返回结果解码​​
  • ​​4> 默认情况下,Feign接收到服务返回的结果后,如何处理?​​
  • ​​三、总结和后续​​

一、前言

在前面的文章:

  1. ​​SpringCloud之Feign实现声明式客户端负载均衡详细案例​​
  2. ​​SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)​​
  3. ​​SpringCloud之OpenFeign的常用配置(超时、数据压缩、日志)​​
  4. ​​SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)​​
  5. ​​SpringBoot启动流程中开启OpenFeign的入口(ImportBeanDefinitionRegistrar详解)​​
  6. ​​源码剖析OpenFeign如何扫描所有的FeignClient​​
  7. ​​源码剖析OpenFeign如何为FeignClient生成动态代理类​​

我们聊了以下内容:

  1. OpenFeign的概述、为什么会使用Feign代替Ribbon?
  2. Feign和OpenFeign的区别?
  3. 详细的OpenFeign实现声明式客户端负载均衡案例
  4. OpenFeign中拦截器RequestInterceptor的使用
  5. OpenFeign的一些常用配置(超时、数据压缩、日志输出)
  6. SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
  7. 在SpringBoot启动流程中开启OpenFeign的入口
  8. OpenFeign如何扫描 / 注册所有的FeignClient
  9. OpenFeign如何为FeignClient生成动态代理类

本文基于OpenFeign低版本(​​SpringCloud 2020.0.x版本之前​​)讨论一下问题:

1、源码剖析OpenFeign的动态代理如何接收和处理请求?
2、OpenFeign的LoadBalancerFeignClient的工作流程?
3、OpenFeign如何与Ribbon、Eureka整合到一起?
4、OpenFeign如何负载均衡选择出一个Server?
5、OpenFeign最终发送的请求地址如何拼接出来?
6、OpenFeign如何将返回的JSON字符串解码为JavaBean?

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>

二、OpenFeign处理HTTP请求

上文(​​源码剖析OpenFeign如何为FeignClient生成动态代理类​​)我们聊了如何为FeignClient生成动态代理类,这里我们接着讨论如何基于动态代理类处理HTTP请求。

0、整体处理请求流程图

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_云原生

以请求http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18为例,请求处理流程如下:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring cloud_02

1、动态代理处理请求的入口

我们知道所有对动态代理对象(T proxy)的所有接口方法的调用,都会交给​​InvocationHandler​​​来处理,此处的​​InvocationHandler​​​是​​ReflectiveFeign​​​的内部类​​FeignInvocationHandler​​​。针对FeignClient的每一个方法都会对应一个​​SynchronousMethodHandler​​。

以​​http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18​​请求调用为例:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_云原生_03

请求打到ServiceBController的greeting()方法后,要调用ServiceAClient#sayHello()方法时,请求会进到ServiceAClient的动态代理类,进而请求交给​​ReflectiveFeign​​​的内部类​​FeignInvocationHandler​​​来处理;在结合JDK动态代理的特性,方法会交给invoke()方法执行,所以动态代理处理请求的入口为:​​ReflectiveFeign​​​的内部类​​FeignInvocationHandler​​​的​​invoke()​​方法:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_04


方法逻辑:

  1. 针对父类Object的equals()、hashCode()、toString()方法直接处理;
  2. 其他方法则从​​dispatch​​中获取Method对应的MethodHandler,然后将方法的执行交给MethodHandler来处理;
  3. ​dispatch​​​是一个以Method为key,MethodHandler为value的Map类型(​​Map<Method, MethodHandler>​​);其是在构建FeignInvocationHandler时,记录了每个FeignClient对应的所有方法 <–> MethodHandler的映射。

invoke()方法中通过方法名称找到Method对应的MethodHandler,这里的MethodHandler为​​SynchronousMethodHandler​​,然后将args参数交给它来处理请求;

2、SynchronousMethodHandler处理请求机制

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring cloud_05

​SynchronousMethodHandler#invoke()​​方法中主要包括两大块:创建请求模板、执行请求并返回Decode后的结果,下面我们分开来看一下。

捎带一提这里的重试机制,其实就是依靠​​Retryer#continueOrPropagate()​​方法中对重试次数的判断,超过最大重试次数抛异常结束流程。

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_负载均衡_06

1)创建请求模板(SpringMvcContract解析方法参数)

在前一篇文章(​​源码剖析OpenFeign如何为FeignClient生成动态代理类​​​)中我们聊到会用SpringMvcContract解析spring mvc的注解,最终拿到的方法对应的请求(RequestTemplate)是​​GET /user/sayHello/{id} HTTP/1.1​​​;但是要生成一个可以访问的请求地址,需要再基于SpringMvcContract去解析@RequestParam注解,将方法的入参,绑定到http请求参数里去。最终将请求处理为​​GET /user/sayHello/1?name=saint&age=18 HTTP/1.1​​。

而​​RequestTemplate template = buildTemplateFromArgs.create(argv);​​负责做这个操作,具体代码逻辑如下:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring cloud_07

以解析@PathVariable为例:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_08

​SpringMvcContrct​​解析逻辑如下:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_负载均衡_09


最后解析出​​@PathVariable("id") Long id​​​对应的值为1,然后将所有的标注了SpringMVC注解的参数都解析完之后,将 参数名 和 对应的value值放到一个命名为​​varBuilder​​的Map中:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_10

接着需要根据varBuilder的内容构建出一个完整的Rest请求(即:将SpringMVC注解标注的参数全部用value值替换、添加到请求中):

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_11

​RequestTemplate#resolve(Map<String, ?> variables)​​​中负责解析并构建完整的RequestTemplate,进到方法中的​​uriTemplate​​​为​​/user/sayHello/{id}​​​,​​variables​​​为上面的​​varBuilder​​​:​​{"id":1,"name":"saint","age":18}​​。

方法中首先将​​“id”: 1​​​替换​​uriTemplate(/user/sayHello/{id})​​​中的{id},得出expanded为​​/user/sayHello/1​​​,然后再将查询参数​​"name":"saint","age":18​​​拼接到请求中,得到最终的URI为:​​/user/sayHello/1?name=saint&age=18​​;返回的RequestTemplate内容为:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_12


​buildTemplateFromArgs.create(argv)​​方法执行完成之后,得到了一个完整的RequestTemplate,下面需要基于这个RequestTemplate来执行请求。

2)执行请求并解码返回值

​SynchronousMethodHandler#executeAndDecode(RequestTemplate, Options)​​方法负责执行请求并解码返回值,具体执行逻辑如下:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_13


方法中主要做三件事:应用所有的​​RequestInterceptor​​(即执行RequestInterceptor#apply()方法)、通过LoadBalancerFeignClient做负载均衡执行请求、使用Decoder对请求返回结果解码 或 处理返回结果。

下面分开来看:

1> 应用所有的RequestInterceptor

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_负载均衡_14

遍历所有的请求拦截器​​RequestInterceptor​​,将每个请求拦截器都应用到RequestTemplate请求模板上面去,也就是让每个请求拦截器都对请求进行处理(调用拦截器的apply(RequestTemplate)方法);

比如这里的requestInterceptors中唯一一个元素​​MyFeignRequestInterceptor​​​,就是我们在(​​SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用)​​)博文中自定义的RequestInterceptor;

  • 其实这里本质上就是基于RequestTemplate,创建一个Request;
  • Request是基于之前的HardCodedTarget(包含了目标请求服务信息的一个Target,服务名也在其中),处理RequestTemplate,生成一个Request。

应用完所有的RequestInterceptor之后,如果Feign日志的隔离级别不等于​​Logger.Level.NONE​​,则打印即将要发送的Request请求日志,如下;

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_15

打印完请求日志之后,会通过​​SynchronousMethodHandler​​​中的成员Client来执行请求,对于OpenFeign旧版本而言,Client是​​LoadBalancerFeignClient​​;

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_云原生_16

2> LoadBalancerFeignClient负载均衡执行请求流程图

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_17

2> LoadBalancerFeignClient负载均衡执行请求详述

基于LoadBalancerFeignClient完成了请求的处理和发送,这里肯定是将HTTP请求发送到对应Server的某个实例上去,同时获取到Response响应。

LoadBalancerFeignClient#execute()方法处理逻辑如下:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_18

方法逻辑解析:

  1. 首先将请求的url封装成一个URI,然后从请求URL地址中,获取到要访问的服务名称​​clientName​​(示例为ServiceA);
  2. 然后将请求URI中的服务名称剔除,比如这里的http://Service-A/user/sayHello/ 变为 http:///user/sayHello/
  3. 接着基于去除了服务名称的uri地址,创建了一个适用于Ribbon的请求(FeignLoadBalancer.RibbonRequest);
  4. 根据服务名从​​SpringClientFactory​​​(Feign上下文)中获取Ribbon相关的配置​​IClientConfig​​​,比如(连接超时时间、读取数据超时时间),如果获取不到,则创建一个​​FeignOptionsClientConfig​​;
  5. 最后根据服务名从​​CachingSpringLoadBalancerFactory​​获取对应的FeignLoadBalancer;在FeignLoadBalancer里封装了ribbon的ILoadBalancer;

既然Feign中集成了Ribbon,那它们是怎么整合到一起的?FeignLoadBalancer中用了Ribbon的那个ILoadBalancer?Feign如何使用Ribbon进行负载均衡?最终发送出去的请求URI是什么样的?

<1> Feign是如何和Ribbon、Eureka整合在一起的?FeignLoadBalancer中用了Ribbon的那个ILoadBalancer?

(1)FeignLoadBalancer中用了Ribbon的那个ILoadBalancer?

FeignLoadBalancer的类继承结构如下:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_云原生_19


【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_20

FeignLoadBalancer间接继承自​​LoadBalancerContext​​​,LoadBalancerContext中有一个​​ILoadBalancer​​​类型的成员,其就是FeignLoadBalancer中集成的Ribbon的ILoadBalancer。从代码执行流程来看,集成的ILoadBalancer为Ribbon默认的​​ZoneAwareLoadBalancer​​:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_21

到这里,可以看到根据服务名获取到的FeignLoadBalancer中组合了Ribbon的​​ZoneAwareLoadBalancer​​负载均衡器。

(2)Ribbon和Eureka的集成?

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_云原生_22


【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_23

Ribbon自己和Eureka集成的流程:Ribbon的配置类RibbonClientConfiguration,会初始化ZoneAwareLoadBalancer并将其注入到Spring容器;ZoneAwareLoadBalancer内部持有跟eureka进行整合的DomainExtractingServerList(Eureka和Ribbon集成的配置类​​EurekaRibbonClientConfiguration​​​(spring-cloud-netflix-eureka-client项目下)中负责将其注入到Spring容器);详细内容可以参考博主的Ribbon系列文章(​​SpringCloud之Ribbon和Erueka/服务注册中心的集成细节​​)。

小结一下:

在spring boot启动,要去获取一个ribbon的ILoadBalancer的时候,会去从那个服务对应的一个独立的spring容器(Ribbon子上下文)中获取;获取到一个服务对应的ZoneAwareLoadBalancer,其中组合了DomainExtractingServerList,DomainExtractingServerList自己会去eureka的注册表里去拉取服务对应的注册表(即:服务的实例列表)。

<2> Feign如何使用Ribbon进行负载均衡?

feign是基于ribbon的ZoneAwareLoadBalancer来进行负载均衡的,从一个server list中选择出来一个server。

接着上面的内容,进入到​​FeignLoadBalancer的executeWithLoadBalancer()​​方法;

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_负载均衡_24


由于​​AbstractLoadBalancerAwareClient​​​是FeignLoadBalancer的父类,FeignLoadBalancer类中没有重写​​executeWithLoadBalancer()​​​方法,进入到​​AbstractLoadBalancerAwareClient#executeWithLoadBalancer()​​方法:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_负载均衡_25

方法逻辑解析:

  1. 首先构建一个LoadBalancerCommand,LoadBalancerCommand刚创建的时候里面的server是null,也就是还没确定要对哪个server发起请求;
  2. command.submit()方法的代码块,本质上是重写了​​LoadBalancerCommand#submit(ServerOperation<T>)​​方法入参ServerOperation的call()方法。
  • call()方法内部根据选择出的Server构造出具体的http请求地址,然后基于底层的http通信组件,发送出去这个请求。
  • call()方法是被内嵌到LoadBalancerCommand#submit()方法中的,也就是在执行LoadBalancerCommand的时候会调用call()方法;
  1. 最后通过command​​.toBlocking().single()​​方法,进行阻塞式的同步执行,获取到响应结果。

从整体来看,ServerOperation中封装了负载均衡选择出来的server,然后直接基于这个server替换掉请求URL中的服务名,拼接出最终的请求URL地址,然后基于底层的http组件发送请求。

LoadBalancerCommand肯定是在某个地方使用Ribbon的ZoneAwareLoadBalancer负载均衡选择出来了一个server,然后将这个server,交给SeerverOpretion中的call()方法去处理。

结合方法的命名找到​​LoadBalancerCommand#selectServer()​​:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_26


【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring cloud_27


selectServer()方法逻辑解析:

在这个方法中,就是直接基于Feign集成的Ribbon的ZoneAwareLoadBalancer的chooseServer()方法,通过负载均衡机制选择了一个server出来。

  • 先通过LoadBalancerContext#​​getServerFromLoadBalancer()​​方法获取到ILoadBalancer;
  • 在利用ILoadBalancer#​​chooseServer()​​方法选择出一个Server。

选择出一个Server之后,再去调用ServerOperation.call()方法,由call()方法拼接出最终的请求URI,发送http请求;

<3> 最终发送出去的请求URI是什么样的?

ServerOperation#call()方法里负责发送请求,在​​executeWithLoadBalancer()​​方法中重写了LoadBalancerCommand#command()方法中入参ServerOperation的call()方法;

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_28


【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring cloud_29

方法逻辑解析:

根据之前处理好的请求URI和Server的地址拼接出真实的地址; 依次拼接http://,服务的IP、服务的Port、请求路径、查询参数,最终体现为:

  1. 原request的uri:GET http:///user/sayHello/1?name=saint&age=18 HTTP/1.1
  2. server地址:192.168.1.3:8082
  3. 拼接后的地址:http://192.168.1.3:8082/user/sayHello/1?name=saint&age=18

接着使用拼接后的地址替换掉request.uri,再调用FeignLoadBalacner#execute()方法发送一个http请求;其中发送请求的超时时间默认为1000ms,即1s;最后返回结果封装到​​FeignLoadBalancer.RibbonResponse​​。

3> 指定Decoder时对请求返回结果解码

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_30


如果配置了decoder,则使用​​Decoder#decode()​​方法对结果进行解码;

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_31

4> 默认情况下,Feign接收到服务返回的结果后,如何处理?

即:未指定decoder时,会直接使用​​AsyncResponseHandler#handleResponse()​​方法处理接收到的服务返回结果:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_负载均衡_32


【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring cloud_33


如果响应结果的returnType为Response时,则将return信息的body()解析成byte数组,放到​​resultFuture​​​的​​RESULT​​​中;然后​​SynchronousMethodHandler#executeAndDecode()​​​方法中通过​​resultFuture.join()​​方法拿到RESULT(即:请求的真正的响应结果)。一般而言,会走到如下else if 分支

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_云原生_34

​decode()​​方法中将response处理为我们要的returnType,比如调用的服务方返回给我们一个JSON字符串,decode()方法中会将其转换为我们需要的JavaBean(即:returnType,当前方法的返回值)。

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_spring_35

deocode()方法中会用到一个Decoder,decoder默认是OptionalDecoder,针对JavaBean返回类型,OptionalDecoder将decode委托给​​ResponseEntityDecoder​​处理。

三、总结和后续

本文小结:

  1. 请求达到FeignClient时,会进入到JDK动态代理类,由​​ReflectiveFeign#FeignInvocationHandler​​​分发处理请求;找到接口方法对应的​​SynchronousMethodHandler​​;
  2. SynchronousMethodHandler中首先使用SpringMvcContract解析标注了SpringMvc注解的参数;然后使用encoder对请求进行编码;
  3. ​RequestInterceptor​​对Request进行拦截处理;
  4. ​LoadBalancerFeignClient​​​通过集成的Ribbon的负载均衡器(​​ZoneAwareLoadBalancer​​)实现负载均衡找到一个可用的Server,交给RibbonRequest组合的Client去做HTTP请求,这里的Client可以是HttpUrlConnection、HttpClient、OKHttp。
  5. 最后Decoder对Response响应进行解码。

至此,OpenFeign低版本(2020.X之前的版本)的主流程源码剖析基本结束,博主在这里用下图做一个阶段性总结:

【云原生&微服务十五】图文源码剖析OpenFeign处理请求流程_微服务_36


细节均在文章中有体现。

后面博主将以一篇文章总结OpenFeign新版本和旧版本之间的差异,主要体现在高版本OpenFeign底层不使用Ribbon做负载均衡。


举报

相关推荐

0 条评论