文章目錄
- 一、前言
- 二、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接收到服務傳回的結果後,如何處理?
- 三、總結和後續
一、前言
在前面的文章:
- SpringCloud之Feign實作聲明式用戶端負載均衡詳細案例
- SpringCloud之OpenFeign實作服務間請求頭資料傳遞(OpenFeign攔截器RequestInterceptor的使用)
- SpringCloud之OpenFeign的常用配置(逾時、資料壓縮、日志)
- SpringCloud之OpenFeign的核心元件(Encoder、Decoder、Contract)
- SpringBoot啟動流程中開啟OpenFeign的入口(ImportBeanDefinitionRegistrar詳解)
- 源碼剖析OpenFeign如何掃描所有的FeignClient
- 源碼剖析OpenFeign如何為FeignClient生成動态代理類
我們聊了以下内容:
- 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()方法直接處理;
- 其他方法則從
中擷取Method對應的MethodHandler,然後将方法的執行交給MethodHandler來處理;
dispatch
-
是一個以Method為key,MethodHandler為value的Map類型(
dispatch
);其是在建構FeignInvocationHandler時,記錄了每個FeignClient對應的所有方法 <–> MethodHandler的映射。
Map<Method, 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位址中,擷取到要通路的服務名稱
(示例為ServiceA);
clientName
- 然後将請求URI中的服務名稱剔除,比如這裡的http://Service-A/user/sayHello/ 變為 http:///user/sayHello/;
- 接着基于去除了服務名稱的uri位址,建立了一個适用于Ribbon的請求(FeignLoadBalancer.RibbonRequest);
- 根據服務名從
(Feign上下文)中擷取Ribbon相關的配置
SpringClientFactory
,比如(連接配接逾時時間、讀取資料逾時時間),如果擷取不到,則建立一個
IClientConfig
;
FeignOptionsClientConfig
- 最後根據服務名從
擷取對應的FeignLoadBalancer;在FeignLoadBalancer裡封裝了ribbon的ILoadBalancer;
CachingSpringLoadBalancerFactory
既然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()方法的代碼塊,本質上是重寫了
方法入參ServerOperation的call()方法。
LoadBalancerCommand#submit(ServerOperation<T>)
- call()方法内部根據選擇出的Server構造出具體的http請求位址,然後基于底層的http通信元件,發送出去這個請求。
- call()方法是被内嵌到LoadBalancerCommand#submit()方法中的,也就是在執行LoadBalancerCommand的時候會調用call()方法;
- 最後通過command
方法,進行阻塞式的同步執行,擷取到響應結果。
.toBlocking().single()
從整體來看,ServerOperation中封裝了負載均衡選擇出來的server,然後直接基于這個server替換掉請求URL中的服務名,拼接出最終的請求URL位址,然後基于底層的http元件發送請求。
LoadBalancerCommand肯定是在某個地方使用Ribbon的ZoneAwareLoadBalancer負載均衡選擇出來了一個server,然後将這個server,交給SeerverOpretion中的call()方法去處理。
結合方法的命名找到
LoadBalancerCommand#selectServer()
:
selectServer()方法邏輯解析:
在這個方法中,就是直接基于Feign內建的Ribbon的ZoneAwareLoadBalancer的chooseServer()方法,通過負載均衡機制選擇了一個server出來。
- 先通過LoadBalancerContext#
方法擷取到ILoadBalancer;
getServerFromLoadBalancer()
- 在利用ILoadBalancer#
方法選擇出一個Server。
chooseServer()
選擇出一個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對請求進行編碼;
-
對Request進行攔截處理;
RequestInterceptor
-
通過內建的Ribbon的負載均衡器(
LoadBalancerFeignClient
)實作負載均衡找到一個可用的Server,交給RibbonRequest組合的Client去做HTTP請求,這裡的Client可以是HttpUrlConnection、HttpClient、OKHttp。
ZoneAwareLoadBalancer
- 最後Decoder對Response響應進行解碼。
至此,OpenFeign低版本(2020.X之前的版本)的主流程源碼剖析基本結束,部落客在這裡用下圖做一個階段性總結: