文章目录

  • 一、前言
  • 二、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. 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、整体处理请求流程图

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_云原生

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_02

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

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

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_03

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_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处理请求机制

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_05

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

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_云原生_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);负责做这个操作,具体代码逻辑如下:

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_07

以解析@PathVariable为例:

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_08

SpringMvcContrct解析逻辑如下:

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_09


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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_云原生_10

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_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内容为:

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_12


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

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

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_13


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

下面分开来看:

1> 应用所有的RequestInterceptor

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_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请求日志,如下;

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_15

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_16

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_17

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

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

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_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的类继承结构如下:

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_19


java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_20

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_21

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

(2)Ribbon和Eureka的集成?

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_22


java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_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()方法;

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_24


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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_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,交给ServerOpretion中的call()方法去处理。

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_微服务_26


java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_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()方法;

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_云原生_28


java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_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时对请求返回结果解码

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring_30


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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_31

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

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_云原生_32


java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_33


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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_负载均衡_34

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

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_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之前的版本)的主流程源码剖析基本结束,博主在这里用下图做一个阶段性总结:

java feign获取数据后将数据返回给另一个feign feign返回结果统一处理_spring cloud_36


细节均在文章中有体现。

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