文章目录
- 一、前言
- 二、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接收到服务返回的结果后,如何处理?
- 三、总结和后续
一、前言
我们聊了以下内容:
- OpenFeign的概述、为什么会使用Feign代替Ribbon?
- Feign和OpenFeign的区别?
- 详细的OpenFeign实现声明式客户端负载均衡案例
- OpenFeign中拦截器RequestInterceptor的使用
- OpenFeign的一些常用配置(超时、数据压缩、日志输出)
- SpringCloud之OpenFeign的核心组件(Encoder、Decoder、Contract)
- 在SpringBoot启动流程中开启OpenFeign的入口
- OpenFeign如何扫描 / 注册所有的FeignClient
- 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、整体处理请求流程图
以请求http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18
为例,请求处理流程如下:
1、动态代理处理请求的入口
我们知道所有对动态代理对象(T proxy)的所有接口方法的调用,都会交给InvocationHandler
来处理,此处的InvocationHandler
是ReflectiveFeign
的内部类FeignInvocationHandler
。针对FeignClient的每一个方法都会对应一个SynchronousMethodHandler
。
以http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18
请求调用为例:
请求打到ServiceBController的greeting()方法后,要调用ServiceAClient#sayHello()方法时,请求会进到ServiceAClient的动态代理类,进而请求交给ReflectiveFeign
的内部类FeignInvocationHandler
来处理;在结合JDK动态代理的特性,方法会交给invoke()方法执行,所以动态代理处理请求的入口为:ReflectiveFeign
的内部类FeignInvocationHandler
的invoke()
方法:
方法逻辑:
- 针对父类Object的equals()、hashCode()、toString()方法直接处理;
- 其他方法则从
dispatch
中获取Method对应的MethodHandler,然后将方法的执行交给MethodHandler来处理;dispatch
是一个以Method为key,MethodHandler为value的Map类型(Map<Method, MethodHandler>
);其是在构建FeignInvocationHandler时,记录了每个FeignClient对应的所有方法 <–> MethodHandler的映射。
invoke()方法中通过方法名称找到Method对应的MethodHandler,这里的MethodHandler为SynchronousMethodHandler
,然后将args参数交给它来处理请求;
2、SynchronousMethodHandler处理请求机制
SynchronousMethodHandler#invoke()
方法中主要包括两大块:创建请求模板、执行请求并返回Decode后的结果,下面我们分开来看一下。
捎带一提这里的重试机制,其实就是依靠Retryer#continueOrPropagate()
方法中对重试次数的判断,超过最大重试次数抛异常结束流程。
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);
负责做这个操作,具体代码逻辑如下:
以解析@PathVariable
为例:
SpringMvcContrct
解析逻辑如下:
最后解析出@PathVariable("id") Long id
对应的值为1,然后将所有的标注了SpringMVC注解的参数都解析完之后,将 参数名 和 对应的value值放到一个命名为varBuilder
的Map中:
接着需要根据varBuilder的内容构建出一个完整的Rest请求(即:将SpringMVC注解标注的参数全部用value值替换、添加到请求中):
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内容为:
buildTemplateFromArgs.create(argv)
方法执行完成之后,得到了一个完整的RequestTemplate,下面需要基于这个RequestTemplate来执行请求。
2)执行请求并解码返回值
SynchronousMethodHandler#executeAndDecode(RequestTemplate, Options)
方法负责执行请求并解码返回值,具体执行逻辑如下:
方法中主要做三件事:应用所有的RequestInterceptor
(即执行RequestInterceptor#apply()方法)、通过LoadBalancerFeignClient做负载均衡执行请求、使用Decoder对请求返回结果解码 或 处理返回结果。
下面分开来看:
1> 应用所有的RequestInterceptor
遍历所有的请求拦截器RequestInterceptor
,将每个请求拦截器都应用到RequestTemplate请求模板上面去,也就是让每个请求拦截器都对请求进行处理(调用拦截器的apply(RequestTemplate)方法);
比如这里的requestInterceptors中唯一一个元素MyFeignRequestInterceptor
,就是我们在(SpringCloud之OpenFeign实现服务间请求头数据传递(OpenFeign拦截器RequestInterceptor的使用))博文中自定义的RequestInterceptor;
- 其实这里本质上就是基于RequestTemplate,创建一个Request;
- Request是基于之前的HardCodedTarget(包含了目标请求服务信息的一个Target,服务名也在其中),处理RequestTemplate,生成一个Request。
应用完所有的RequestInterceptor之后,如果Feign日志的隔离级别不等于Logger.Level.NONE
,则打印即将要发送的Request请求日志,如下;
打印完请求日志之后,会通过SynchronousMethodHandler
中的成员Client来执行请求,对于OpenFeign旧版本而言,Client是LoadBalancerFeignClient
;
2> LoadBalancerFeignClient负载均衡执行请求流程图
2> LoadBalancerFeignClient负载均衡执行请求详述
基于LoadBalancerFeignClient完成了请求的处理和发送,这里肯定是将HTTP请求发送到对应Server的某个实例上去,同时获取到Response响应。
LoadBalancerFeignClient#execute()
方法处理逻辑如下:
方法逻辑解析:
- 首先将请求的url封装成一个URI,然后从请求URL地址中,获取到要访问的服务名称
clientName
(示例为ServiceA);- 然后将请求URI中的服务名称剔除,比如这里的http://Service-A/user/sayHello/ 变为 http:///user/sayHello/;
- 接着基于去除了服务名称的uri地址,创建了一个适用于Ribbon的请求(FeignLoadBalancer.RibbonRequest);
- 根据服务名从
SpringClientFactory
(Feign上下文)中获取Ribbon相关的配置IClientConfig
,比如(连接超时时间、读取数据超时时间),如果获取不到,则创建一个FeignOptionsClientConfig
;- 最后根据服务名从
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的类继承结构如下:
FeignLoadBalancer间接继承自LoadBalancerContext
,LoadBalancerContext中有一个ILoadBalancer
类型的成员,其就是FeignLoadBalancer中集成的Ribbon的ILoadBalancer。从代码执行流程来看,集成的ILoadBalancer为Ribbon默认的ZoneAwareLoadBalancer
:
到这里,可以看到根据服务名获取到的FeignLoadBalancer中组合了Ribbon的ZoneAwareLoadBalancer
负载均衡器。
(2)Ribbon和Eureka的集成?
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()
方法;
由于AbstractLoadBalancerAwareClient
是FeignLoadBalancer的父类,FeignLoadBalancer类中没有重写executeWithLoadBalancer()
方法,进入到AbstractLoadBalancerAwareClient#executeWithLoadBalancer()
方法:
方法逻辑解析:
- 首先构建一个LoadBalancerCommand,LoadBalancerCommand刚创建的时候里面的server是null,也就是还没确定要对哪个server发起请求;
- command.submit()方法的代码块,本质上是重写了
LoadBalancerCommand#submit(ServerOperation<T>)
方法入参ServerOperation的call()方法。
- call()方法内部根据选择出的Server构造出具体的http请求地址,然后基于底层的http通信组件,发送出去这个请求。
- call()方法是被内嵌到LoadBalancerCommand#submit()方法中的,也就是在执行LoadBalancerCommand的时候会调用call()方法;
- 最后通过command
.toBlocking().single()
方法,进行阻塞式的同步执行,获取到响应结果。
从整体来看,ServerOperation中封装了负载均衡选择出来的server,然后直接基于这个server替换掉请求URL中的服务名,拼接出最终的请求URL地址,然后基于底层的http组件发送请求。
LoadBalancerCommand肯定是在某个地方使用Ribbon的ZoneAwareLoadBalancer负载均衡选择出来了一个server,然后将这个server,交给ServerOpretion中的call()方法去处理。
结合方法的命名找到LoadBalancerCommand#selectServer()
:
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()方法;
方法逻辑解析:
根据之前处理好的请求URI和Server的地址拼接出真实的地址; 依次拼接http://,服务的IP、服务的Port、请求路径、查询参数,最终体现为:
- 原request的uri:GET http:///user/sayHello/1?name=saint&age=18 HTTP/1.1
- server地址:192.168.1.3:8082
- 拼接后的地址:http://192.168.1.3:8082/user/sayHello/1?name=saint&age=18
接着使用拼接后的地址替换掉request.uri,再调用FeignLoadBalacner#execute()方法发送一个http请求;其中发送请求的超时时间默认为1000ms,即1s;最后返回结果封装到FeignLoadBalancer.RibbonResponse
。
3> 指定Decoder时对请求返回结果解码
如果配置了decoder,则使用Decoder#decode()
方法对结果进行解码;
4> 默认情况下,Feign接收到服务返回的结果后,如何处理?
即:未指定decoder时,会直接使用AsyncResponseHandler#handleResponse()
方法处理接收到的服务返回结果:
如果响应结果的returnType为Response时,则将return信息的body()解析成byte数组,放到resultFuture
的RESULT
中;然后SynchronousMethodHandler#executeAndDecode()
方法中通过resultFuture.join()
方法拿到RESULT(即:请求的真正的响应结果)。一般而言,会走到如下else if 分支
decode()
方法中将response处理为我们要的returnType,比如调用的服务方返回给我们一个JSON字符串,decode()方法中会将其转换为我们需要的JavaBean(即:returnType,当前方法的返回值)。
deocode()方法中会用到一个Decoder,decoder默认是OptionalDecoder,针对JavaBean返回类型,OptionalDecoder将decode委托给ResponseEntityDecoder
处理。
三、总结和后续
本文小结:
- 请求达到FeignClient时,会进入到JDK动态代理类,由
ReflectiveFeign#FeignInvocationHandler
分发处理请求;找到接口方法对应的SynchronousMethodHandler
;- SynchronousMethodHandler中首先使用SpringMvcContract解析标注了SpringMvc注解的参数;然后使用encoder对请求进行编码;
RequestInterceptor
对Request进行拦截处理;LoadBalancerFeignClient
通过集成的Ribbon的负载均衡器(ZoneAwareLoadBalancer
)实现负载均衡找到一个可用的Server,交给RibbonRequest组合的Client去做HTTP请求,这里的Client可以是HttpUrlConnection、HttpClient、OKHttp。- 最后Decoder对Response响应进行解码。
至此,OpenFeign低版本(2020.X之前的版本)的主流程源码剖析基本结束,博主在这里用下图做一个阶段性总结:
细节均在文章中有体现。
后面博主将以一篇文章总结OpenFeign新版本和旧版本之间的差异,主要体现在高版本OpenFeign底层不使用Ribbon做负载均衡。