天天看點

Feign聲明式http用戶端源碼分析

作者:NEDHOME
Feign聲明式http用戶端源碼分析

from:cnblogs.com/cuzzz/p/17154932.htm

一丶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掃描與注冊#

Feign聲明式http用戶端源碼分析

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

  • 導入的類是ImportSelector類型
  • 反射執行個體化ImportSelector
  • 如果此ImportSelector實作了BeanClassLoaderAware,BeanFactoryAware,EnvironmentAware,EnvironmentAware,ResourceLoaderAware會回調對應的方法
  • 調用目前ImportSelector的selectImports,然後遞歸執行處理@Import注解的方法,也就是說可以導入一個具備@Import的類,如果沒有``@Import`那麼當中配置類解析
  • 導入的類是ImportBeanDefinitionRegistrar類型
  • 反射執行個體化ImportBeanDefinitionRegistrar,然後加入到importBeanDefinitionRegistrars集合中後續會回調其registerBeanDefinitions
  • 既不是ImportBeanDefinitionRegistrar也不是ImportSelector,将導入的類當做配置類處理,後續會判斷條件注解是否滿足,然後解析導入的類,并且解析其父類

這裡導入FeignClientsRegistrar 是一個ImportBeanDefinitionRegistrar,因而會回調其registerBeanDefinitions

Feign聲明式http用戶端源碼分析

這裡我們關注下 registerFeignClients 此方法會掃描标記有@FeignClient注解的接口,包裝成BeanDefinition 注冊到BeanDefinitionRegistry,後續在feignClient被依賴注入的時候,根據此BeanDefinition進行執行個體化

1.掃描FeignClient#

  • 如果我們在@EnableFeignClients注解中的clients 指定了類,那麼隻會将這些FeignClient 包裝成AnnotatedGenericBeanDefinition
  • 否則使用ClassPathScanningCandidateComponentProvider 掃描生成BeanDefinition
  • ClassPathScanningCandidateComponentProvider 允許 重寫isCandidateComponent方法自定義什麼樣的BeanDefinition是我們的候選者,以及添加TypeFilter來進行限定(其addExcludeFilter,addIncludeFilter可以設定排除什麼,包含什麼)
  • 這個getScanner方法,對isCandidateComponent進行了重寫,限定不能是内部類且不能是注解
  • 哪些包下的類需要掃描
  • 如果@EnableFeignClients指定了value,basePackages,basePackageClasses,那麼優先掃描指定的包,如果沒有,那麼掃描@EnableFeignClients标注配置類所在的包
  • 如何掃描
  • 調用ClassPathScanningCandidateComponentProvider#findCandidateComponents進行掃描
  • 底層還是基于ClassLoader#getResources擷取資源

2.處理每一個FeignClient 接口的 BeanDefinition#

Feign聲明式http用戶端源碼分析
  • 注冊每一個FeignClient的個新化配置
  • openFeign 支援每一個 FeignClient接口使用個新化的配置,基于父子容器實作,這點我們在後續進行分析
  • 注冊FeignClient 的BeanDefinition
  • 這裡非常關鍵,因為我們的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 接口實作類

Feign聲明式http用戶端源碼分析

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

Feign聲明式http用戶端源碼分析

1.Feign個性化配置上下文#

Feign聲明式http用戶端源碼分析

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

Feign聲明式http用戶端源碼分析

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

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

Feign聲明式http用戶端源碼分析

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

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

Feign聲明式http用戶端源碼分析

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

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

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

Feign聲明式http用戶端源碼分析

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

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

Feign聲明式http用戶端源碼分析

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

Feign聲明式http用戶端源碼分析

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

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

Feign聲明式http用戶端源碼分析

這些配置都将映射FeignClientProperties中

Feign聲明式http用戶端源碼分析

3.生成動态代理對象#

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

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

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

Feign聲明式http用戶端源碼分析

3.2 對于指定應用名稱的FeignClient#

生成動态代理對象最終調用到Feign(實作類ReflectiveFeign)#newInstance

Feign聲明式http用戶端源碼分析

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處理

Feign聲明式http用戶端源碼分析

3.2.2使用InvocationHandlerFactory建構出InvocationHandler并進行jdk動态代理。#

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

最後使用JDK動态代理生成代理對象。

至此FeignClient接口的動态代理對象生成,那麼如何發送請求呢,如果将入參轉化為http請求封包,如何将http響應轉換為實體對象呢?

Feign聲明式http用戶端源碼分析

四丶Feign 如何發送請求#

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

Feign聲明式http用戶端源碼分析

1.InvocationHandlerFactory 生成InvocationHandler#

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

2.ReflectiveFeign.FeignInvocationHandler#

Feign聲明式http用戶端源碼分析

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

3.SynchronousMethodHandler 發現請求#

Feign聲明式http用戶端源碼分析

3.1根據參數構造RequestTemplate#

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

  1. 将http請求頭,表單參數,路徑參數,根據參數的值設定到RequestTemplate
  2. 3.2.1中,我們知道Feign會使用AnnotatedParameterProcessor解析參數注解内容,并解析@RequestMapping注解的内容,放在對應的資料結構中,然後當真正調用的時候,它會根據之前解析的内容,将參數中的值設定到RequestTemplate中,這部分會填充url,表單參數,請求頭等。
  3. 使用Encoder對@RequestBody注解标注的參數解析到RequestTemplate
  4. Encoder會被回調encoder方法,其中最重要的是SpringEncoder,它負責解析
  5. 這裡并沒有說必須标注@RequestBody注解,即使不标注,且沒有标注@RequestParam,@RequestHead,@PathVariable,都會一股腦,進行序列化寫入到body,看來是不支援@RequestPart這種multipart/form-data格式的參數。

3.2 使用Retryer控制重試#

Feign聲明式http用戶端源碼分析

重試器提供兩個方法

  • clone:拷貝,注意如果使用淺拷貝,需要考慮多線程情況下的并發問題
  • continueOrPropagate:繼續,還是傳播(即抛出)異常,如果抛出異常,代表不在重試,反之繼續重試

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

  1. Feign預設配置是不走重試政策的,當發生RetryableException異常時直接抛出異常。
  2. 并非所有的異常都會觸發重試政策,隻有發送請求的過程中抛出 RetryableException 異常才會觸發異常政策。
  3. 在預設Feign配置情況下,隻有在網絡調用時發生 IOException 異常時,才會抛出RetryableException,也是就是說連結逾時、讀逾時等不不會觸發此異常。

下面是Feign預設的重試政策,總結就是,請求失敗後擷取間隔多久重試(響應頭可指定,或者使用1.5的幂次計算),然後讓目前線程休眠,後發起重試

Feign聲明式http用戶端源碼分析

3.3 發送請求并解碼#

Feign聲明式http用戶端源碼分析

發送請求并解碼的邏輯在executeAndDecoder方法中,這個方法外層是一個while(true)的死循環,如果抛出的異常是RetryableExecption那麼交由Retryer來控制是重試,還是抛出異常結束重試。如果抛出的不是重試異常那麼将直接結束,不進行重試。

整個excuteAndDecode 可用分為三步:

  1. 回調RequestInterceptor,并将RequestTemplate轉化為Request
  2. RequestInterceptor的apply方法在此被回調,我們可自定義自己的RequestIntereptor實作token透傳等操作
  3. RequestTemplate(請求模闆)轉化為Request(請求對象),這裡可了解為什麼叫請求模闆,在FeignClient被動态代理前,就對接口中方法進行了掃描,為每一個方法要發送怎樣的封包制定了模闆(RequestTemplate)後面針對參數的不同來補充模闆,然後用模闆生成請求對象,這何嘗不是一種單一職責的體驗!下面是RequestTemplate如何轉變為Request對象
  4. 使用Client發送請求
  5. Client具備兩個重要的實作:Default(使用jdk自帶的HttpConnection發送http請求,也支援Https)LoadBalancerFeignClient(基于Ribbon實作負載均衡功能增強的裝飾器)
  6. LoadBalancerFeignClient本質是一個裝飾器,内部持有了一個Client實作類執行個體,使用Ribbon根據請求應用名和負載均衡政策選擇合适的執行個體,然後重構url(替換成實際的域名或者ip)然後再使用Client發送http請求。
  7. Feign預設使用的就是 LoadBalancerFeignClient裝飾後的Default(沒有連接配接池,對每一個請求都保持一個長連接配接),建議替換成其他的Http元件,如OkHttp,Apache的HttpClient等。
  8. 使用Decoder對響應進行解碼
  9. 如果FeignClient接口方法傳回值類型為Response,那麼将直接傳回Response,而不會進行解碼。
  10. 如果請求碼為[200,300)的範圍,那麼将使用Decoder進行解碼,解析成接口方法指定的類型
  11. 如果請求為404,且指定了需要解碼404,那麼同使用Decoder進行解碼
  12. 其餘情況使用ErrorDecoder進行解碼,根據響應資訊決定抛出異常(如果抛出RetryException 将由Retryer控制重試,還是結束)

3.3.1 Decoder解碼#

Feign聲明式http用戶端源碼分析

可看到隻要是非FeignException的RuntimeExeption會被包裝成DecoderExeption抛出。下面我們看下Decoder的實作類

Feign聲明式http用戶端源碼分析
  • Default
  • 主要是對Byte數組的支援
  • StringDecoder
  • 主要是将body轉成字元串
  • SpringDecoder
  • 底層使用HttpMessageConverter對body進行裝換,會從響應頭中拿出Content-Type決定使用什麼政策,通常傳回json這裡将使用基于Jackson的MappingJackson2HttpMessageConverter進行轉換。(這部分在springmvc源碼中有過介紹,不再贅述)
  • ResponeEntityDecoder
  • 一個Decoder裝飾器實作對ResponeEntity的支援

3.3.2 ErrorDecoder#

Feign聲明式http用戶端源碼分析

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

Feign聲明式http用戶端源碼分析

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

Feign聲明式http用戶端源碼分析
Feign聲明式http用戶端源碼分析

五丶對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中。

繼續閱讀