註解源碼
① 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%異常率,大大降低,至此完美解決線上問題。
② 怎麼找到spring註解解析器的源碼
下面用的是4.2.5的源碼。
從這個文件開始看:META-INF/spring.handlers
文件里的內容是http://www.springframework.org/schema/mvc=org.springframework.web.servlet.config.MvcNamespaceHandler
MvcNamespaceHandler源碼:
③ Spring 源碼(九)@Autowired註解實現原理(Spring Bean的自動裝配)
@Autowired 註解的實現過程,其實就是Spring Bean的自動裝配過程。通過看@Autowired源碼注釋部分我們可以看到 @Autowired 的實現是通過 後置處理器中實現的。
在分析自動裝配前我們先來介紹一下上面的這些後置處理器。
BeanPostProcessor有兩個方法, 和 。它們分別在任何bean初始化回調之前或之後執行(例如InitializingBean的afterPropertiesSet方法或自定義init-method方法之前或者之後), 在這個時候該bean的屬性值已經填充完成了,並且我們返回的bean實例可能已經是原始實例的包裝類型了。例如返回一個 FactoryBean 。
繼承自 BeanPostProcessor 介面。主要多提供了以下三個方法。
該方法是在Bean實例化目標對象之前調用,返回的Bean對象可以代理目標,從而有效的阻止了目標Bean的默認實例化。
跟進源碼我們可以看出,如果此方法返回一個非null對象,則Bean創建過程將被短路。唯一應用的進一步處理是來自已配置BeanPostProcessors的回調。
該方法執行在通過構造函數或工廠方法在實例化bean之後但在發生Spring屬性填充(通過顯式屬性或自動裝配)之前執行操作。這是在Spring的自動裝配開始之前對給定的bean實例執行自定義欄位注入的理想回調。如果該方法返回false,那麼它會阻斷後續 後置處理器的執行,並且會阻止後續屬性填充的執行邏輯。
在工廠將給定屬性值應用於給定bean之前,對它們進行後處理。允許檢查是否滿足所有依賴關系,例如基於bean屬性設置器上的「 Required」註解。還允許替換要應用的屬性值,通常是通過基於原始PropertyValues創建新的MutablePropertyValues實例,添加或刪除特定值來實現。
智能實例化Bean的後處理器,主要提供了三個方法。
預測從此處理器的回調最終返回的bean的類型。
確定使用實例化Bean的構造函數。
獲取提早暴露的Bean的引用,提早暴露的Bean就是只完成了實例化,還未完成屬性賦值和初始化的Bean。
合並Bean的定義信息的後處理方法,該方法是在Bean的實例化之後設置值之前調用。
後置處理器主要負責對添加了@Autowired和@Value註解的元素實現自動裝配。所以找到需要自動裝配的元素,其實就是對@Autowired和@Value註解的解析。
後置處理器,找出需要自動裝配的元素是在 . 這個方法中實現的,調用鏈路如下:
從鏈路上我們可以看到,找到需要自動裝配的元素是在 findAutowiringMetadata 方法中實現的,該方法會去調用 buildAutowiringMetadata 方法構建元數據信息。如果註解被載入屬性上將會被封裝成 AutowiredFieldElement 對象;如果註解加在方法上,那麼元素會被封裝成 AutowiredMethodElement 對象。這里兩個對象的 inject 方法將最後完成屬性值的注入,主要區別就是使用反射注入值的方式不一樣。源碼如下:
尋找需要自動裝配過程:
後置處理器注入屬性值是在 postProcessPropertyValues 方法中實現的。源碼如下:
從這里的源碼我們可以看出 AutowiredFieldElement 和 AutowiredMethodElement 完成自動裝配都是先去容器中找對應的Bean,然後通過反射將獲取到的Bean設置到目標對象中,來完成Bean的自動裝配。
我們可以看出Spring Bean的自動裝配過程就是:
在自動裝配過程中還涉及循環依賴的問題,有興趣的可以看下這篇文章 Spring 源碼(八)循環依賴
④ 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的方法時會發起 遠程調用