天天看點

【雲原生&微服務十五】圖文源碼剖析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、整體處理請求流程圖

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

以請求​

​http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18​

​為例,請求處理流程如下:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

1、動态代理處理請求的入口

我們知道所有對動态代理對象(T proxy)的所有接口方法的調用,都會交給​

​InvocationHandler​

​​來處理,此處的​

​InvocationHandler​

​​是​

​ReflectiveFeign​

​​的内部類​

​FeignInvocationHandler​

​​。針對FeignClient的每一個方法都會對應一個​

​SynchronousMethodHandler​

​。

以​

​http://localhost:9090/ServiceB/user/sayHello/1?name=saint&age=18​

​請求調用為例:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

請求打到ServiceBController的greeting()方法後,要調用ServiceAClient#sayHello()方法時,請求會進到ServiceAClient的動态代理類,進而請求交給​

​ReflectiveFeign​

​​的内部類​

​FeignInvocationHandler​

​​來處理;在結合JDK動态代理的特性,方法會交給invoke()方法執行,是以動态代理處理請求的入口為:​

​ReflectiveFeign​

​​的内部類​

​FeignInvocationHandler​

​​的​

​invoke()​

​方法:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

方法邏輯:

  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處理請求機制

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

​SynchronousMethodHandler#invoke()​

​方法中主要包括兩大塊:建立請求模闆、執行請求并傳回Decode後的結果,下面我們分開來看一下。

捎帶一提這裡的重試機制,其實就是依靠​

​Retryer#continueOrPropagate()​

​方法中對重試次數的判斷,超過最大重試次數抛異常結束流程。

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

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);​

​負責做這個操作,具體代碼邏輯如下:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

以解析​

​@PathVariable​

​為例:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

​SpringMvcContrct​

​解析邏輯如下:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

最後解析出​

​@PathVariable("id") Long id​

​​對應的值為1,然後将所有的标注了SpringMVC注解的參數都解析完之後,将 參數名 和 對應的value值放到一個命名為​

​varBuilder​

​的Map中:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

接着需要根據varBuilder的内容建構出一個完整的Rest請求(即:将SpringMVC注解标注的參數全部用value值替換、添加到請求中):

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

​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内容為:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

​buildTemplateFromArgs.create(argv)​

​方法執行完成之後,得到了一個完整的RequestTemplate,下面需要基于這個RequestTemplate來執行請求。

2)執行請求并解碼傳回值

​SynchronousMethodHandler#executeAndDecode(RequestTemplate, Options)​

​方法負責執行請求并解碼傳回值,具體執行邏輯如下:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

方法中主要做三件事:應用所有的​

​RequestInterceptor​

​(即執行RequestInterceptor#apply()方法)、通過LoadBalancerFeignClient做負載均衡執行請求、使用Decoder對請求傳回結果解碼 或 處理傳回結果。

下面分開來看:

1> 應用所有的RequestInterceptor
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

周遊所有的請求攔截器​

​RequestInterceptor​

​,将每個請求攔截器都應用到RequestTemplate請求模闆上面去,也就是讓每個請求攔截器都對請求進行處理(調用攔截器的apply(RequestTemplate)方法);

比如這裡的requestInterceptors中唯一一個元素​

​MyFeignRequestInterceptor​

​​,就是我們在(​​SpringCloud之OpenFeign實作服務間請求頭資料傳遞(OpenFeign攔截器RequestInterceptor的使用)​​)博文中自定義的RequestInterceptor;

  • 其實這裡本質上就是基于RequestTemplate,建立一個Request;
  • Request是基于之前的HardCodedTarget(包含了目标請求服務資訊的一個Target,服務名也在其中),處理RequestTemplate,生成一個Request。

應用完所有的RequestInterceptor之後,如果Feign日志的隔離級别不等于​

​Logger.Level.NONE​

​,則列印即将要發送的Request請求日志,如下;

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

列印完請求日志之後,會通過​

​SynchronousMethodHandler​

​​中的成員Client來執行請求,對于OpenFeign舊版本而言,Client是​

​LoadBalancerFeignClient​

​;

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
2> LoadBalancerFeignClient負載均衡執行請求流程圖
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
2> LoadBalancerFeignClient負載均衡執行請求詳述

基于LoadBalancerFeignClient完成了請求的處理和發送,這裡肯定是将HTTP請求發送到對應Server的某個執行個體上去,同時擷取到Response響應。

​LoadBalancerFeignClient#execute()​

​方法處理邏輯如下:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

方法邏輯解析:

  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的類繼承結構如下:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

FeignLoadBalancer間接繼承自​

​LoadBalancerContext​

​​,LoadBalancerContext中有一個​

​ILoadBalancer​

​​類型的成員,其就是FeignLoadBalancer中內建的Ribbon的ILoadBalancer。從代碼執行流程來看,內建的ILoadBalancer為Ribbon預設的​

​ZoneAwareLoadBalancer​

​:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

到這裡,可以看到根據服務名擷取到的FeignLoadBalancer中組合了Ribbon的​

​ZoneAwareLoadBalancer​

​負載均衡器。

(2)Ribbon和Eureka的內建?

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

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()​

​方法;

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

由于​

​AbstractLoadBalancerAwareClient​

​​是FeignLoadBalancer的父類,FeignLoadBalancer類中沒有重寫​

​executeWithLoadBalancer()​

​​方法,進入到​

​AbstractLoadBalancerAwareClient#executeWithLoadBalancer()​

​方法:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

方法邏輯解析:

  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()​

​:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

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()方法;

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

方法邏輯解析:

根據之前處理好的請求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時對請求傳回結果解碼
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

如果配置了decoder,則使用​

​Decoder#decode()​

​方法對結果進行解碼;

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
4> 預設情況下,Feign接收到服務傳回的結果後,如何處理?

即:未指定decoder時,會直接使用​

​AsyncResponseHandler#handleResponse()​

​方法處理接收到的服務傳回結果:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程
【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

如果響應結果的returnType為Response時,則将return資訊的body()解析成byte數組,放到​

​resultFuture​

​​的​

​RESULT​

​​中;然後​

​SynchronousMethodHandler#executeAndDecode()​

​​方法中通過​

​resultFuture.join()​

​方法拿到RESULT(即:請求的真正的響應結果)。一般而言,會走到如下else if 分支

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

​decode()​

​方法中将response處理為我們要的returnType,比如調用的服務方傳回給我們一個JSON字元串,decode()方法中會将其轉換為我們需要的JavaBean(即:returnType,目前方法的傳回值)。

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

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之前的版本)的主流程源碼剖析基本結束,部落客在這裡用下圖做一個階段性總結:

【雲原生&amp;微服務十五】圖文源碼剖析OpenFeign處理請求流程

繼續閱讀