SpringCloud 原始碼學習筆記2——Feign宣告式http客戶端原始碼分析

Cuzzz發表於2023-02-25

系列文章目錄和關於我

一丶Feign是什麼

Feign是一種宣告式、 模板化的HTTP客戶端。在Spring Cloud中使用Feign,可以做到使用HTTP請求訪問遠端服務,就像呼叫本地方法一一樣的, 開發者完全感知不到這是在呼叫遠端方法,更感知不到在訪問HTTP請求。接下來介紹一下Feign的特性,具體如下:

  • 可插拔的註解支援,和SpringBoot結合後還支援SpringMvc中的註解
  • 支援可插拔的HTTP編碼器和解碼器。
  • 支援Hystrix和它的Fallback。
  • 支援Ribbon的負載均衡。
  • 支援HTTP請求和響應的壓縮。

Feign是一個宣告式的Web Service客戶端,它的目的就是讓Web Service 呼叫更加簡單。它整合了Ribbon和Hystrix,從而不需要開發者針對Feign對其進行整合。Feign 還提供了HTTP請求的模板,透過編寫簡單的介面和註解,就可以定義好HTTP請求的引數、格式、地址等資訊。Feign 會完全代理HTTP的請求,在使用過程中我們只需要依賴注人Bean,然後呼叫對應的方法傳遞引數即可。

二丶@EnableFeignClients ——Feign Client掃描與註冊

image-20230116152112543

通常這個註解標註在 SpringBoot專案啟動類,或者配置類,其本質是@Import(FeignClientsRegistrar.class) 。在 SpringBoot原始碼學習1——SpringBoot自動裝配原始碼解析+Spring如何處理配置類的 中我們講到過,spring中的ConfigurationClassPostProcessor中會使用ConfigurationClassParser解析配置類,對於@Import註解根據註解匯入的類有如下處理

  • 匯入的類是ImportSelector型別

    反射例項化ImportSelector

    如果此ImportSelector實現了BeanClassLoaderAware,BeanFactoryAwareEnvironmentAware,EnvironmentAware,ResourceLoaderAware會回撥對應的方法

    呼叫當前ImportSelectorselectImports,然後遞迴執行處理@Import註解的方法,也就是說可以匯入一個具備@Import的類,如果沒有``@Import`那麼當中配置類解析

  • 匯入的類是ImportBeanDefinitionRegistrar型別

    反射例項化ImportBeanDefinitionRegistrar,然後加入到importBeanDefinitionRegistrars集合中後續會回撥其registerBeanDefinitions

  • 既不是ImportBeanDefinitionRegistrar也不是ImportSelector,將匯入的類當做配置類處理,後續會判斷條件註解是否滿足,然後解析匯入的類,並且解析其父類

這裡匯入FeignClientsRegistrar 是一個ImportBeanDefinitionRegistrar,因而會回撥其registerBeanDefinitions

image-20230117143738317

這裡我們關注下 registerFeignClients 此方法會掃描標記有@FeignClient註解的介面,包裝成BeanDefinition 註冊到BeanDefinitionRegistry,後續在feignClient被依賴注入的時候,根據此BeanDefinition進行例項化

1.掃描FeignClient

  • 如果我們在@EnableFeignClients註解中的clients 指定了類,那麼只會將這些FeignClient 包裝成AnnotatedGenericBeanDefinition

  • 否則使用ClassPathScanningCandidateComponentProvider 掃描生成BeanDefinition

    ClassPathScanningCandidateComponentProvider 允許 重寫isCandidateComponent方法自定義什麼樣的BeanDefinition是我們的候選者,以及新增TypeFilter來進行限定(其addExcludeFilter,addIncludeFilter可以設定排除什麼,包含什麼)

    image-20230117151942928

    這個getScanner方法,對isCandidateComponent進行了重寫,限定不能是內部類且不能是註解

    image-20230117152057318

  • 哪些包下的類需要掃描

    如果@EnableFeignClients指定了valuebasePackages,basePackageClasses,那麼優先掃描指定的包,如果沒有,那麼掃描@EnableFeignClients標註配置類所在的包

  • 如何掃描

    呼叫ClassPathScanningCandidateComponentProvider#findCandidateComponents進行掃描

    image-20230117160223831

    底層還是基於ClassLoader#getResources獲取資源

2.處理每一個FeignClient 介面的 BeanDefinition

image-20230117162547413

  • 註冊每一個FeignClient的個新化配置

    openFeign 支援每一個 FeignClient介面使用個新化的配置,基於父子容器實現,這點我們在後續進行分析

  • 註冊FeignClientBeanDefinition

    這裡非常關鍵,因為我們的FeignClient 介面的BeanDefinition 其記錄的class 是 一個介面,spring無法例項化,這裡要設定為FactoryBean,然後後續才能呼叫FactoryBean#getObject,生成介面的動態代理類,從而讓動態代理類物件實現傳送Http請求的功能

    BeanDefinitionBuilder definition = BeanDefinitionBuilder
          .genericBeanDefinition(FeignClientFactoryBean.class);
    

    其中會生成一個FeignClientFactoryBean的BeanDefinition,並且將@FeignClient中的url,path,name,contextId等都呼叫BeanDefinition.addPropertyValue進行設定,這樣spring在例項化的使用會據此來對FeignClientFactoryBean物件的屬性進行填充

    其中最關鍵的是,記錄了原FeignClient介面的型別,因為FeignClientFactoryBean使用的是Jdk動態代理,需要介面型別。

    至此feignClient型別的bean都被載入並註冊到BeanDefinitionRegistry,後續在Spring容器重新整理時便會觸發FeignClient的例項化

三丶FeignClient 是如何例項化動態代理物件的

在其他spring bean需要注入FeignClient 的時候,將觸發FeignClient 的例項化。會先例項化FeignClientFactoryBean,並且進行屬性填充(之前將@FeignClient註解中的內容,使用BeanDefinition.addPropertyValue進行了繫結,後面由spring據此進行屬性填充),然後呼叫getObject方法例項化出原本FeignClient 介面實現類

image-20230221185518919

下面我們看下FeignClient是如何生成代理類的(這裡設計到編碼器,解碼器等元件,這部分內容再傳送請求的章節進行解釋,這一章節關注於FeignClient是如何生成代理物件的)

image-20230221194421921

1.Feign個性化配置上下文

image-20230221190407434

FeignContext是Feign允許每一個FeignClient進行個性化配置的關鍵。

image-20230221191414079

FeignContext是Spring上下文中的一個Bean,其內部使用一個Map儲存每一個Feign對應的個性化配置ApplicationContext

1.1.何為Feign的個性化配置ApplicationContext

image-20230221191656203

如上圖這種使用方式,可以為一個FeignClient指定特定的配置類,然後再這個配置類中使用@Bean注入特定的Encoder(將FeignClient入參轉化Http報文的一部分的一個元件),Decoder(將Http請求解析為介面出參的一個元件)等。

上圖中AClientConfig會被註冊到A這個FeignClient的個性化ApplicationContext(下圖的黃色部分)

image-20230221191414079

1.2 FeignClient 個性化配置ApplicationContext的父ApplicationContext是Spring容器

上圖中,我們標註了AClient個性化配置ApplicationContext的父容器時Spring上下文(SpringBoot啟動後建立的上下文,最大的上下文)。這樣設計的目的是,如果當前個性化配置中沒有指定Decoder 那麼使用預設的容器中的Decoder,如果指定了那麼使用個性化的配置。

2.構建Feign建立者,並選擇使用的Decoder,Encoder

image-20230221200129618

2.1 獲取個性化配置,或者使用預設配置

上圖中,獲取Encoder,Decoder等都使用get方法,get方法內容如下

image-20230221200306671

利用了AnnotationConfigApplicationContext#getBean會去父容器找的特點,實現個性化配置不存在,使用預設配置,具體邏輯在DefaultListableBeanFactory中,如下

image-20230221200602940

2.2 configureFeign根據配置檔案 進一步進行配置

feign還支援我們在配置檔案中,進行若干配置,下面展示一部分配置

image-20230221201318720

這些配置都將對映FeignClientProperties

image-20230221201436290

3.生成動態代理物件

3.1 對於@FeignClient指定url的特殊處理

如果@FeignClient註解指定了url,將無法進行負載,比如我們業務系統,指定請求外部系統的API,這個API和我們並不在同一個註冊中心,那麼便無從進行負載均衡。這裡會將原本的LoadBalanceFeignClient中的delegate拿出來(這個delegate被LoadBalanceFeignClient裝飾,再請求之前會先根據註冊中心和負載均衡選擇一個例項,然後重構url,然後再使用delegate傳送請求)

最終生成代理物件的邏和指定服務名的FeignClient殊途同歸

image-20230221202016500

3.2 對於指定應用名稱的FeignClient

生成動態代理物件最終呼叫到Feign(實現類ReflectiveFeign)#newInstance

image-20230221204152053

3.2.1 SpringMvcContract 解析方法生成MethodHandler

其中生成的MethodHandler這一步將根據SpringMvcContract(springmvc合約)去解析介面方法上的註解,最關鍵的是構建出RequestTemplate物件,它是請求的模板,後續Http請求物件由它轉化而來。

這一步還會解析@RequestMapping註解(包括@PostMapping這種複合註解)

  • 解析類上和方法上的value,解析出請求的目的地址,儲存到RequestTemplate
  • 解析@RequestMapping中的heads,會根據環境變數中的內容得到對應的值,在請求的時候自動攜帶對應的頭
  • 解析@RequestMapping的生產produces,報文Accept攜帶這部分內容
  • 解析@RequestMapping的消費consumes,報文頭Content-Type攜帶這部分內容

這一步還會解析以下三個方法上的註解:

  • 將@RequestParam標註的引數,新增到RequestTemplate的Map<String, Collection<String>> queries,最終會表單的格式加入到Http報文的body
  • 將@PathVariable標註的引數,新增到List<String> formParams,最終會以路徑引數的形式加入到Http路徑請求中
  • 將@RequestHead標註的引數,新增到Map<String, Collection<String>> headers,最終會加入到http請求報文的頭部

解析的操作交由AnnotatedParameterProcessor#processArgument處理

image-20230223222306010

3.2.2使用InvocationHandlerFactory構建出InvocationHandler並進行jdk動態代理。

這裡產生的InvocationHandler(一般為ReflectiveFeign.FeignInvocationHandler,如果由熔斷配置那麼是HystrixInvocationHandler,此類會在呼叫失敗的時候,回撥FeignClient對應的fallBack)

最後使用JDK動態代理生成代理物件。

至此FeignClient介面的動態代理物件生成,那麼如何傳送請求呢,如果將入參轉化為http請求報文,如何將http響應轉換為實體物件呢?

image-20230225173331201

四丶Feign 如何傳送請求

上面我們已經分析了FeignClient是如何被掃描,被包裝成BeanDefinition註冊到BeanDefinitionRegistry中,也看了FeignClientFactoryBean是如何生成FeignClient介面代理類的,至此我們可用知道的我們平時依賴注入的介面其實是FeignClientFactoryBean#getObject生成的動態代理物件。那麼這個代理物件是如何傳送請求的暱?

image-20230222230601698

1.InvocationHandlerFactory 生成InvocationHandler

這一步使用工廠模式生成InvocationHandler,如果沒有hystrix熔斷的配置,那麼這裡生成的是ReflectiveFeign.FeignInvocationHandler,反之生成的是HystrixInvocationHandler

2.ReflectiveFeign.FeignInvocationHandler

image-20230222231152217

這裡是從dispatch根據Method 獲取到MethodHandler(通常是SynchronousMethodHandler

3.SynchronousMethodHandler 發現請求

image-20230222231609284

3.1根據引數構造RequestTemplate

這裡使用RequestTemplate.Factory(請求模板物件)生成RquestTemplate,比較關鍵的點是:

  1. 將http請求頭,表單引數,路徑引數,根據引數的值設定到RequestTemplate

    3.2.1中,我們知道Feign會使用AnnotatedParameterProcessor解析引數註解內容,並解析@RequestMapping註解的內容,放在對應的資料結構中,然後當真正呼叫的時候,它會根據之前解析的內容,將引數中的值設定到RequestTemplate中,這部分會填充url,表單引數,請求頭等。

  2. 使用Encoder對@RequestBody註解標註的引數解析到RequestTemplate

    image-20230223224712952

    Encoder會被回撥encoder方法,其中最重要的是SpringEncoder,它負責解析

    image-20230223225149861

    這裡並沒有說必須標註@RequestBody註解,即使不標註,且沒有標註@RequestParam@RequestHead,@PathVariable,都會一股腦,進行序列化寫入到body,看來是不支援@RequestPart這種multipart/form-data格式的引數。

3.2 使用Retryer控制重試

image-20230223230015826

重試器提供兩個方法

  • clone:複製,注意如果使用淺複製,需要考慮多執行緒情況下的併發問題
  • continueOrPropagate:繼續,還是傳播(即丟擲)異常,如果丟擲異常,代表不在重試,反之繼續重試

我們可以透過在容器中,或者FeignClient個性化配置類中,注入Retryer實現重試邏輯,如果不注入使用的是預設的實現Retryer.Default。這裡需要注意

  1. Feign預設配置是不走重試策略的,當發生RetryableException異常時直接丟擲異常。
  2. 並非所有的異常都會觸發重試策略,只有傳送請求的過程中丟擲 RetryableException 異常才會觸發異常策略。
  3. 在預設Feign配置情況下,只有在網路呼叫時發生 IOException 異常時,才會丟擲RetryableException,也是就是說連結超時、讀超時等不不會觸發此異常。

下面是Feign預設的重試策略,總結就是,請求失敗後獲取間隔多久重試(響應頭可指定,或者使用1.5的冪次計算),然後讓當前執行緒休眠,後發起重試

image-20230223231747489

3.3 傳送請求並解碼

image-20230225152403418

傳送請求並解碼的邏輯在executeAndDecoder方法中,這個方法外層是一個while(true)的死迴圈,如果丟擲的異常是RetryableExecption那麼交由Retryer來控制是重試,還是丟擲異常結束重試。如果丟擲的不是重試異常那麼將直接結束,不進行重試。

整個excuteAndDecode 可用分為三步:

  1. 回撥RequestInterceptor,並將RequestTemplate轉化為Request

    image-20230225153749871

    RequestInterceptorapply方法在此被回撥,我們可自定義自己的RequestIntereptor實現token透傳等操作

    image-20230225154445807

    RequestTemplate(請求模板)轉化為Request(請求物件),這裡可理解為什麼叫請求模板,在FeignClient被動態代理前,就對介面中方法進行了掃描,為每一個方法要傳送怎樣的報文制定了模板(RequestTemplate)後面針對引數的不同來補充模板,然後用模板生成請求物件,這何嘗不是一種單一職責的體驗!下面是RequestTemplate如何轉變為Request物件

    image-20230225154341704

  2. 使用Client傳送請求

    image-20230225154932024

    Client具備兩個重要的實現:Default(使用jdk自帶的HttpConnection傳送http請求,也支援Https)LoadBalancerFeignClient(基於Ribbon實現負載均衡功能增強的裝飾器)

    LoadBalancerFeignClient本質是一個裝飾器,內部持有了一個Client實現類例項,使用Ribbon根據請求應用名和負載均衡策略選擇合適的例項,然後重構url(替換成實際的域名或者ip)然後再使用Client傳送http請求。

    Feign預設使用的就是 LoadBalancerFeignClient裝飾後的Default(沒有連線池,對每一個請求都保持一個長連線),建議替換成其他的Http元件,如OkHttp,Apache的HttpClient等。

    image-20230225160204589

  3. 使用Decoder對響應進行解碼

    • 如果FeignClient介面方法返回值型別為Response,那麼將直接返回Response,而不會進行解碼。
    • 如果請求碼為[200,300)的範圍,那麼將使用Decoder進行解碼,解析成介面方法指定的型別
    • 如果請求為404,且指定了需要解碼404,那麼同使用Decoder進行解碼
    • 其餘情況使用ErrorDecoder進行解碼,根據響應資訊決定丟擲異常(如果丟擲RetryException 將由Retryer控制重試,還是結束)

    image-20230225161451870

3.3.1 Decoder解碼

image-20230225161605655

可看到只要是非FeignException的RuntimeExeption會被包裝成DecoderExeption丟擲。下面我們看下Decoder的實現類

image-20230225162436495

  • Default

    主要是對Byte陣列的支援

    image-20230225162651752

  • StringDecoder

    主要是將body轉成字串

    image-20230225162718395

  • SpringDecoder

    底層使用HttpMessageConverter對body進行裝換,會從響應頭中拿出Content-Type決定使用什麼策略,通常返回json這裡將使用基於JacksonMappingJackson2HttpMessageConverter進行轉換。(這部分在springmvc原始碼中有過介紹,不再贅述)

  • ResponeEntityDecoder

    一個Decoder裝飾器實現對ResponeEntity的支援

    image-20230225163609432

3.3.2 ErrorDecoder

image-20230225164114565

ErrorDecoder存在一個實現類Default,它會根據響應頭中的Retry-After丟擲重試異常,反之丟擲FeignExeption,如果是重試異常那麼,由Retryer控制重試還是結束

image-20230225164651517

但是遺憾的是,這個重試通常是不生效的,它需要服務提供方返回重試時間塞到Retry-After的頭中,且會使用下面這個SimpleDateFormat加鎖進行序列化,序列化為Date,我們中國人的服務估計不是這樣的時間格式,且現在企業級的服務都是返回code,data,message這樣的響應體,http響應狀態碼基本上都是200,所以想實現這種重試,需要我們自定義Decoder(不是ErrorDecoder)去實現

image-20230225165012526

image-20230225175151180

五丶對Feign進行擴充套件

可看到Feign是很模組的化的,也提供了很多擴充套件的介面讓我們做自定義,以下是筆者做過(或者見過)的一些擴充套件。

1.自定義RequestIntercptor實現認證資訊的透傳

class AService{
    void process(){
        feign.getSomething(xxxx);
    }
}

我們服務中,需要AService呼叫process的時候,將認證資訊透傳到微服務提供方,我們自定義RequestIntercptor拿到當前的請求資訊,然後獲取其中的認證資訊透過apply方法寫入到RequestTemplate的head中。

2.SpringMVC 統一返回結果集解包裝

基於SpringBoot的服務,透過使用SpringMVC ResponseAdvice實現統一包裝集,即使業務邏輯丟擲異常,也透過ExeptionHandler進行統一包裝,包裝形式如下

{
   "code":"業務錯誤碼",
    "data": "業務資料",
    "message":"錯誤資訊"
}

這就導致,我們微服務呼叫方,使用feign的時候,結果返回值也是這種統一返回結果集形式的物件,需要自己對code進行校驗,然後選擇丟擲異常,還是反序列化為目標物件。

我們可以實現自己的Decoder結果這一問題!在Decoder中對code進行判斷,決定丟擲異常,還是序列化data。但是需要注意Decoder丟擲的異常,都將被包裝為FeignExeption或者DecodeExption,所以呼叫方還需要針對這兩種異常配置ExeptionHandler

3.自定義RequestIntercptor實現分散式鏈路追蹤

原理同一,只不過拿的是呼叫方請求中的 traceId,將traceId,寫到RequestTemplate的head中。

相關文章