feign源碼
㈠ SpringCloud系列之Feign-6.Feign上下文構建解析
1.首先在構建上下文的入口是在FeignClientFactoryBean的getObject方法
2.首先第一步就是構建了FeignContext類
3.然後我們進入到feign這個方法中看下:
可以看到首先獲取兩個日誌對象一個FeignLoggerFactory,一個Logger,然後就是把這兩個日誌對象賦值給feign這個對象,我們看的Builder其實類似於方法糖,讓賦值更加方便而已,類似於lombok中的@builder註解一樣。
除了賦值日誌對象以外,還有加密解密的一個賦值操作,這個是url解析過程中也是非常常見的。然後其他的我們暫時不關注,只要知道這里是初始化了feign對象,一個空的對象,構造器。
4.然後我們繼續在主方法往下走
在上一個帖子上我們注意到url是空的,那麼在這里就需要使用到了,從源碼中看到如果url是空的,那麼就使用服務名稱作為ip了,然後如果不是http開頭,這里就會給一個http的前綴上去,因為feign也是基於eureka的http介面的。
我們在看下下一個方法啊 cleanPaht();
這里其實還是拼裝url的操作,在@FeignClient註解中有一個path的屬性,這個屬性幹嘛用的呢,如圖:
而這個path我們最後是不需要/的,所以這里其實就是格式化一下而已,正確的拼接好url的操作。
5.主方法繼續
這里就是構造動態代理對象的地方了。
我們點進去看:
可以看到首先調用了getOptional這個方法,然後使用了
傳入了一個調用的服務名稱就是 eureka-client這個服務名稱,還有一個 feign.Client的類。
其實這個方法就是從上下文中拿到調用了這個服務的介面作為一個client。
然後把client放到Feign對象中,再構建一個Targeter類
6.然後我們進入targeter.target這個方法看看這個最核心的地方
這塊邏輯做了一個判斷,看這個feign是不是hystrix的feign,如果是就走上面的邏輯,否則就走下面的邏輯,降級熔斷啊之類的邏輯,但我們注意到不論走哪個,最終都會走feign.target(target);
7.然後我們進入 feign.target 這個方法:
我們繼續進入newInstance這個方法:
上面的三個方法沒看懂,我們暫且跳過,看到target通過斷點我們可以知道,type就是IService,getMethods就是獲取到了FeignClient那個介面下面的所有方法了,現在就是遍歷所有方法了,我們現在懂點看下遍歷中做的邏輯是什麼
8.首先進行判斷,看他是對象嗎,顯然這個是方法不是對象所以跳過,然後Util.isDefault(method)我們看下:
看著大致意思就是判斷這個是不是介面而且修飾符必須是這幾種;
所以這個判斷也沒進去,最後執行了下面這個方法:
我們查看下Feign.configKey(target.type(), method) 這個是什麼:
發現是這個FeignClient介面的名稱#方法名稱;
所以這里就在methodToHandler.put如一個方法,還有方法的Handler,所以說這個介面雖然沒有實現類,但是轉發到了其他的實現類中,那麼這個實現類到底是什麼,我們繼續往下面看:
然後就把這個handler作為參數創建了一個代理proxy並返回,然後如果我們通過Feign訪問的話其實訪問的就是代理proxy,但是最終幹活的其實就是handler。
㈡ Spring Cloud Feign實現自定義復雜對象傳參
現我們服務提供端有如下的根據用戶查詢條件獲取滿足條件的用戶列表controller介面
我們在使用Feign構建遠程服務請求客戶端的時候,會發現Feign官方版本是不支持GET請求傳遞自定義的對象,當我們的請求參數很多的時候,我們只能選擇以下兩種方式:
那麼我們希望能有一種方式 保持跟controller完全一致只需要傳遞自定義的對象 ,既讓服務提供端開發人員爽,也讓服務消費端開發人員爽,兩全其美。既然Feign官方不支持,那我們就自己動手擼源碼,自己來實現。
對比之前的@RequestParam和Map用法,方法參數變少了,User對象復用了,對服務提供端和消費端都更方便了
最近在調研spring cloud版本升級,發現新版的Feign也支持了自定義對象傳參,實現方式大同小異
㈢ open feign的超時配置及源碼跟蹤
content:[2021-02-26 18:43:59.939][http-nio-8080-exec-11][ERROR][c.l.c.c.r.RestCartServiceImpl:50] [reqId:] <getMyCartDetail> 操作異常: feign.RetryableException: Read timed out executing POST http://cic-proct-service/cic-proct/getProctListByIds at feign.FeignException.errorExecuting(FeignException.java:249) at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:129) at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89) at feign.ReflectiveFeign Proxy162.getProctListByIds(Unknown Source)
問題:在application.yaml中配置的feign的超時一直不生效,在網上也找不到相應的合理解決方案,通常的答案是對feign依賴的底層ribbon設置超時來解決,但這個不是官方推薦的方式,所以我就產生了跟蹤源碼的興趣。
解決方案:首先我是通過對@FeignClient註解源碼進行了斷點跟蹤,看下在服務啟動時,spring boot是如何載入OpenFeign的超時配置的,最後跟蹤到
FeignClientFactoryBean的configureUsingProperties方法讀取了feign的配置
跟蹤到這兒,就可以知道在我們對@FeignClient不進行重寫或者不額外指定configuration的配置時,他是默認載入default配置,而這個default配置默認超時是2秒
要想更改這個默認超時時間需要按照官方推薦的格式,才能生效,如下:
按照配置設置後,又對該服務配置做了驗證和壓測,在4核8g的mac本上,查詢購物車服務支持150個並發,異常率0%,比之前的70%異常率,大大降低,至此完美解決線上問題。
㈣ SpringCloud系列之Feign-5.@EnableFeignClients底層機制深度解析
@EnableFeignClients 源碼比較值得一讀,讀完之後我們就學會了如何自己寫一個註解並成功運用起來了
1.首先我們進入到 @EnableFeignClients註解裡面可以看到這個註解裡面聲明了幾個屬性,通過名稱大概可以看到比如basePackages應該是包路徑,value的話應該是個basePackages別名,我們暫且不管,看下這個註解上面有個@Import的註解,點進去這個類來看一下
2.如圖可以看到FeignClientsRegistrar這個類實現了三個Spring的類,根據名稱大概猜一下,第一個應該是關於類定義注冊的類,第二個第三個相信大家可能用過或者了解過就是Spring的Aware的一些類,大致就是載入資源或者環境變數的類
那我們看一下ImportBeanDefinitionRegistrar類裡面是什麼:
可以看到裡面定義了一個方法,那麼我們重新返回到FeignClientsRegistrar看下他是怎麼實現這個方法的:
這個方法主要幹了兩件事兒,第一個方法是注冊了默認的配置信息,第二個就是注冊FeignClients,我們挨個一個個詳細的看下:
到了較為詳細的源碼時候,如果看不懂,我們最好是打斷點,當我們啟動了Eureka-server,Eureka-client,然後再啟動Feign-consumer的時候,斷點就可以進來,我們可以看到一些傳參的信息:
可以看到這個metedata裡面的數據剛好就是在啟動類上面的三個註解,並且還帶有三個註解的屬性信息,下面再給大家看下主類對照下就懂了:
然後我們繼續往下面學習:
這一步就比較好理解了,我們拿到EnableFeignClients這個註解的屬性信息。
然後就是拼接了一個名稱,這個名稱就是啟動類的前端加了個default.而已,然後registry沒變還是傳參過來,defaultAttrs.get("defaultConfiguration")這個屬性從剛剛斷點來看也是空的。調用了一個registerClientConfiguration方法:
這個方法就是使用了Spring的BeanDefinitionBuilder把FeignClientSpecification這個bean給注冊到Spring容器中了。
然後我們繼續放回到主方法中看下一個方法:
這個方法根據名稱registerFeignClients來說應該就是注冊FeignClients類了,進入方法中,第一個scanner我們看下:
有點看不懂,沒關系,猜一下,可能是掃描類的工具把。
我們繼續王下面走,scanner載入了一個resourceLoader這個類,這個類我們可以查一下,他是Spring框架中與資源相關的類,然後再往下看
下面還是獲取主類中的註解EnableFeignClients的屬性信息
再往下,我們可以從圖中看到在屬性中獲取關於clients的信息,但是沒有,然後scanner就加了一個類似過濾器的東東,然後調用了getBasePackages的方法
下面我們看下getBasePackages方法:
看過之前的方法,這個方法就好理解了,首先就還是獲取EnableFeignClients的所有屬性信息,然後把值都給取出來,取得屬性分別是value還是basePackages等關於包路徑的屬性值,如果都沒有獲取到,就獲取一個默認的包路徑
這個包路徑斷點可以看到就是主類的包路徑,所以整體上看,這些邏輯就是首先看註解中有沒有關於FeignClient的包路徑信息,如果沒有配置,那程序就准備從主程序所在的包路徑下找所有的FeignClient了。
我們再詳細看下:
然後繼續看這個registerClientConfiguration方法:
這個方法我們之前看過就是把某個類載入到Spring中所以繼續下一步,看registerFeignClient方法:
獲取到這個FeignClient的所有屬性之後,我們就進行數據處理,把屬性信息都賦值給definition
這個方法表示我們把這個類以 按照類型注入 作為屬性,然後
這塊邏輯就是為了防止兩個有同一個父類的FeignClient出現問題所做的,我們應該都遇到過一個問題就是 使用 @Autowire注入類的時候發現報錯,說是有兩個類不知道注入哪一個,而如果其中一個有@primary註解的話,spring是會優先注入這個類的。
下面就沒什麼了,直到最後執行了
整個@EnableFeignClients的實現到此執行完畢,這個註解的源碼相對來說看起來算是比較清晰明了了,而且對於我們如果有做一個新註解的需求的話,完全可以參照著做,非常具有模板意義。
㈤ springcloud feign返回Map解析處理
繼上回feign多參數處理的坑剛解決完,又出現了新的問題
springcloud feign多參數調用解析處理方法
源碼地址: https://gitee.com/ttx_urey/feign-multiple-param
feign返回為Map時,底層解析Map的key或value為對象時,自動把對象解析為Map,導致調用的時候類型不匹配,那沒辦法,只能繼續解決
解決思路:指定返回對象為Map時,調用自己的解析器來處理
先新增一個自己的解析器:
只用管
和
這兩個方法,因為使用ObjectMapper解析Map時,我們需要知道Map的Key和Value的類型,前面兩個沒有Type參數的同名方法管不了
接下來,直接創建Bean,Spring會自動注入解析器列表裡面
還有一個問題,就是如果不在feign的生產者的pom文件中把jackson-dataformat-xml的jar包去掉的話,feign默認返回的格式是xml,就不能用ObjectMapper了(雖然可以用XmlMapper,但是我不喜歡XML),所以需要去掉jar包
以防萬一,在commom模塊中再把feign-jackson的jar包加上
在瀏覽器打開 http://127.0.0.1:8100/test2
後台日誌中可以看到
㈥ Feign踩坑記錄:JSON parse error
1.跟蹤拋出異常的堆棧,發現在對返回結果的json解析中拋出異常
2.為什麼會解析json失敗呢,我們單獨調用feign對應的介面是正常的,json也是正常可以解析的
3.難道feign的處理過返回的內容,又去跟了下fegin處理過程發現從response獲取到流並沒有任何異常,難道是出在了源頭?但是源頭又沒有任何異常,此時思緒已經混亂,試著在google上查找有沒有相關的問題,沒想到在feign的github上找到類似問題 https://github.com/OpenFeign/feign/issues/934
4.問題已然發現,就是響應的內容經過gzip編碼,feign默認的Client不支持gzip解碼。那麼在此跟蹤一下feign的源碼查看處理過程,從入口 SynchronousMethodHandler 開始,在122行開始獲取響應內容
最終在 Logger 的102行找到響應流的讀取,讀取的流程如下:
5.最終問題出在feign使用默認的HttpURLConnection,並沒有經過任何處理,導致讀取的是gzip壓縮後的內容。此時我們可以將其置換為Httpclient,其內部 ResponseContentEncoding 的 process 方法,取出了Content-Encoding並判斷不為空,然後獲取對應的處理方式。
上面所說feign默認的Client不支持gzip解碼可能容易引起歧義,應該是fegin默認的Client對響應流不支持對gzip後的位元組流進行解析,所以在序列化成對象時會存在解析問題。如果一定要接收可以使用 ResponseEntity<byte[]> 來接收,這樣feign就不會對其反序列化了。至於 feign.compression.request.enabled=true , feign.compression.response.enabled=true 配置的內容在 , ,大致可以看出只是在請求頭添加了Header而已
2020/3/13
spring已添加支持,SpringCloud版升級到Hoxton即可
https://github.com/spring-cloud/spring-cloud-openfeign/pull/230
2020/12/01
對於仍然存在問題的夥伴,可以直接使用OkHttp設置為feign的客戶端(因為okhttp是默認支持gzip壓縮),不需要關注spring cloud版本; 最簡單的方案,也是最推薦的方案 。
㈦ FeignClient 測試
(1)Maven 依賴
(2)接收請求的 DemoController
(3)調用遠程介面的 FeignClient
執行三次 main() 方法,服務端控制台輸出:
源碼解析:
㈧ Spring Cloud Feign 源碼分析 - FeignClientFactoryBean
關於Feign的啟動原理分析,參照另一篇 Spring Cloud Feign 源碼分析 - feign啟動原理
書接上文,上篇最後提到所有帶@FeignClient註解的interface都被封裝成FeignClientFactoryBean的BeanDefinition。從名字上可以得知這個類是一個FactoryBean。關於FactoryBean的介紹參考...
因此直接找getObject()。
getTarget方法首先獲取FeignContext的對象,基於這個context對當前feign的配置信息存放到Builder中。
首先實例化bean:FeignContext
FeignContext的定義在FeignAutoConfiguration
第一次除了創建新的FeignContext對象之外,還設置了一組configurations,
這組configurations是FeignClientSpecification類型,通過autowired注入。
在掃描EnableFeignClients和各個FeignClient時,將configuration對應的class封裝成了FeignClientSpecification的BeanDefinition,這里從容器中取出來創建對象注入到configurations
通過斷點可以看到這里有15個FeignClientSpecification的對象
一個是default.開頭的在啟動類里配置的configuration,剩下的都是FeignClient的configuration。
FeignContext繼承了NamedContextFactory,對應的范型就是FeignClientSpecification,看下NamedContextFactory構造方法
這里設置了默認的defaultConfigType,feign里用的是FeignClientsConfiguration,定義了一系列的默認值。
在獲取到FeignContext之後,開始封裝Feign.Builder。
首先通過context實例化FeignLoggerFactory的對象,因為context是NamedContextFactory的子類,會給每個contextId創建一個獨立的上下文,每一個k-v會存儲在FeignContext的全局context中,key就是contextId
這三個方法的實現完全體現了NamedContextFactory的作用:
給每個name創建一個單獨的ApplicationContext子上下文對象,後續凡是這個name的ioc操作,都由獨立的ApplicationContext來完成,name之間的context相互隔離。所有的子上下文保存在了Map<String, > contexts中。
在創建Context時,補充了configuration的設置:
首先(1的位置),從全局的configurations查找是否定義了只對當前name生效的configuration,也就是判斷在當前name所屬的FeignClient註解上是否定義了configuration。如果定義過,將這個configuration的Class封裝成BeanDefinition注冊到本name的子上下文中。
接著(2的位置),從全局的configurations查找是否定義了全局配置,也就是@EnableFeignClients的defaultConfiguration的值,這里固定前綴是default.。
如果也存在,就也將這個defaultConfiguration的Class封裝成BeanDefinition注冊到本name的子上下文中。
第一次調用完畢get方法後,給每個FeignClient創建的FeignContext就完成了configuration初始化的動作,後面的所有操作,如配置encoder、decoder都是給當前的子上下文內注冊BeanDefinition。最後將所有配置封裝成Builder返回。
在getTarget()構造完成builder屬性之後,開始了整個請求調度過程。
先看第一段:
如果沒有url屬性,就用name來處理,把http:// + name + path 拼裝成url,執行loadBalance()
首先實例化Client的bean對象,默認返回LoadBalancerFeignClient的實例。
從LoadBalancerFeignClient的構造方法可以看到,這里使用了delegate的設計模式來代理Client.Default,擴展execute的實現。
然後則繼續實例化Targeter的bean。默認有兩種實現類。
我這里返回HystrixTargeter。調用target方法。
這里重點看下feign.target(target)的實現。
可以看到,通過build()構造了一個ReflectiveFeign的對象,將一系列feign的參數封裝成了SynchronousMethodHandler和ParseHandlersByName。封裝的這兩個對象都是為了給後面newInstance用的。
newInstance返回了擴展後的Targeter的代理類。
下面介紹下newInstance的詳細過程。
apply方法就是給feignClient的每個方法都封裝了一個SynchronousMethodHandler,
factory.create(...)就是為了根據當前方法的各個參數+new SynchronousMethodHandler.Factory定義的默認參數來構造SynchronousMethodHandler
key對應的是類名#方法名,如:MasterClientLocal#getPersons()。
for循環則是為了封裝methodToHandler,k-v分別是reflect的Method和SynchronousMethodHandler。遍歷完成後,構建一個InvocationHandler的實現類:FeignInvocationHandler
通過傳入target和dispatch,其實本質就是在調用SynchronousMethodHandler的invoke方法。而invoke方法則是擴展了http的調用動作,包括請求重試,decode處理,decode404判斷等。
最重要的執行則是client.execute(...);
client有兩種實現類:
Default是帶url的execute的實現,封裝了最普通的http調用。
LoadBalanceFeignClient是eureka的實現,通過獲取server列表來實現loadBalance。
也就是最開始getTarget() 方法的兩段不同的實現過程的最本質區別。
至此,FeignClientFactoryBean的源碼分析告一段落。
本人通過delegate方式在此基礎上實現了traceId的跨feign傳遞。將在下一篇文章中做具體說明。
㈨ Feign源碼解析二
本文會基於Feign源碼,看看Feign到底是怎麼實現遠程調用
上文中,我們的 user-service 服務需要調用遠程的 order-service 服務完成一定的業務邏輯,而基本實現是order-service提供一個spi的jar包給user-service依賴,並且在user-service的啟動類上添加了一個註解
這個註解就是@EnableFeignClients,接下來我們就從這個註解入手,一步一步解開Feign的神秘面紗
該註解類上的注釋大概的意思就是:
掃描那些被聲明為 Feign Clients (只要有 org.springframework.cloud.openfeign.FeignClient 註解修飾的介面都是Feign Clients介面)的介面
下面我們繼續追蹤源碼,看看到底什麼地方用到了這個註解
利用IDEA的查找調用鏈快捷鍵,可以發現在.class類型的文件中只有一個文件用到了這個註解
OK,下面主要就是看這個類做了什麼
通過UML圖我們發現該類分別實現了 ImportBeanDefinitionRegistrar , ResourceLoaderAware 以及 EnvironmentAware 介面
這三個介面均是spring-framework框架的spring-context模塊下的介面,都是和spring上下文相關,具體作用下文會分析
總結下來就是利用這兩個重要屬性,一個獲取應用配置屬性,一個可以載入classpath下的文件,那麼FeignClientsRegistrar持有這兩個東西之後要做什麼呢?
上面將bean配置類包裝成 FeignClientSpecification ,注入到容器。該對象非常重要,包含FeignClient需要的 重試策略 , 超時策略 , 日誌 等配置,如果某個FeignClient服務沒有設置獨立的配置類,則讀取默認的配置,可以將這里注冊的bean理解為整個應用中所有feign的默認配置
由於 FeignClientsRegistrar 實現了 ImportBeanDefinitionRegistrar 介面,這里簡單提下這個介面的作用
我們知道在spring框架中,我們如果想注冊一個bean的話主要由兩種方式:自動注冊/手動注冊
知道了 ImportBeanDefinitionRegistrar 介面的作用,下面就來看下 FeignClientsRegistrar 類是何時被載入實例化的
通過IDEA工具搜索引用鏈,發現該類是在註解@EnableFeignClients上被import進來的,文章開始的圖片中有
這里提下@Import註解的作用
該註解僅有一個屬性value,使用該註解表明導入一個或者多個@Configuration類,其作用和.xml文件中的<import>等效,其允許導入@Configuration類,ImportSelector介面/ImportBeanDefinitionRegistrar介面的實現,也同樣可以導入一個普通的組件類
注意,如果是XML或非@Configuration的bean定義資源需要被導入的話,需要使用@ImportResource註解代替
這里我們導入的FeignClientsRegistrar類正是一個ImportBeanDefinitionRegistrar介面的實現
FeignClientsRegistrar重寫了該介面的 registerBeanDefinitions 方法,該方法有兩個參數註解元數據 metadata 和bean定義注冊表 registry
該方法會由spring負責調用,繼而注冊所有標注為@FeignClient註解的bean定義
下面看registerBeanDefinitions方法中的第二個方法,在該方法中完成了所有@FeignClient註解介面的掃描工作,以及注冊到spring中,注意這里注冊bean的類型為 FeignClientFactoryBean ,下面細說
總結一下該方法,就是掃描@EnableFeignClients註解上指定的basePackage或clients值,獲取所有@FeignClient註解標識的介面,然後將這些介面一一調用以下 兩個重要方法 完成 注冊configuration配置bean 和注冊 FeignClient bean
斷點位置相當重要
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
這里是利用了spring的代理工廠來生成代理類,即這里將所有的 feignClient的描述信息 BeanDefinition 設定為 FeignClientFactoryBean 類型,該類繼承自FactoryBean,因此這是一個代理類,FactoryBean是一個工廠bean,用作創建代理bean,所以得出結論,feign將所有的 feignClient bean定義的類型包裝成 FeignClientFactoryBean
最終其實就是存入了BeanFactory的beanDefinitionMap中
那麼代理類什麼時候會觸發生成呢? 在spring 刷新容器時 ,會根據beanDefinition去實例化bean,如果beanDefinition的beanClass類型為代理bean,則會調用其 T getObject() throws Exception; 方法生成代理bean,而我們實際利用注入進來的FeignClient介面就是這些一個個代理類
這里有一個需要注意的點,也是開發中會遇到的一個 啟動報錯點
如果我們同時定義了兩個不同名稱的介面 (同一個包下/或依賴方指定全部掃描我們提供的 @FeignClient ),且這兩個 @FeignClient 介面註解的 value/name/serviceId 值一樣的話,依賴方拿到我們的提供的spi依賴,啟動類上 @EnableFeignClients 註解掃描能同時掃描到這兩個介面,就會 啟動報錯
原因就是Feign會為每個@FeignClient註解標識的介面都注冊一個以serviceId/name/value為key,FeignClientSpecification類型的bean定義為value去spring注冊bean定義,又默認不允許覆蓋bean定義,所以報錯
官方提示給出的解決方法要麼改個@FeignClient註解的serviceId,name,value屬性值,要麼就開啟spring允許bean定義覆寫
至此我們知道利用在springboot的啟動類上添加的@EnableFeignClients註解,該註解中import進來了一個手動注冊bean的 FeignClientsRegistrar注冊器 ,該注冊器會由spring載入其 registerBeanDefinitions方法 ,由此來掃描所有@EnableFeignClients註解定義的basePackages包路徑下的所有標注為@FeignClient註解的介面,並將其注冊到spring的bean定義Map中,並實例化bean
下一篇博文中,我會分析為什麼我們在調用(@Resource)這些由@FeignClient註解的bean的方法時會發起 遠程調用
㈩ Feign-靈活的使用Hystrix熔斷(自定義CommandKey)
Feign可以直接去集成Hystrix熔斷。具體配置: Hystrix熔斷&&Feign熔斷
但是配置時,卻不是很靈活,只是支持 default 和 類名#方法名() 的配置。這就不能對類或者一組方法進行統一的配置。
源碼改造:
項目啟動後,會遍歷@Feign註解類中的每一個方法調用 create 生成 Hystrix的配置key ,也可以是線程池的key。
源碼位置: feign.hystrix.HystrixInvocationHandler#toSetters 如下圖所示。
yml的配置:
註:默認情況下,Feign-Hystrix會使用 服務名 作為CommandGroup,使用 類名#方法名 作為CommandKey。
而在yml配置: 全局配置是default的配置,而實例配置為commandKey配置。
更多的配置,可以參考 Hystrix(2)— 相關配置
[享學Feign] 十二、Feign通過feign-hystrix模塊使其擁有熔斷、降級能力