做者 | Spring Cloud Alibaba 高級開發工程師洛夜
來自公衆號阿里巴巴中間件投稿
前段時間 Hystrix 宣佈再也不維護以後(Hystrix 中止開發。。。Spring Cloud 何去何從?),Feign 做爲一個跟 Hystrix 強依賴的組件,必然會有所擔憂後續的使用。java
做爲 Spring Cloud Alibaba 體系中的熔斷器 Sentinel,Sentinel 目前整合了 Feign,本文對整合過程作一次總結,歡迎你們討論和使用。git
Feign 是一個 Java 實現的 Http 客戶端,用於簡化 Restful 調用。github
Feign 跟 OkHttp、HttpClient 這種客戶端實現理念不同。Feign 強調接口的定義,接口中的一個方法對應一個 Http 請求,調用方法即發送一個 Http 請求;OkHttp 或 HttpClient 以過程式的方式發送 Http 請求。Feign 底層發送請求的實現能夠跟 OkHttp 或 HttpClient 整合。spring
要想整合 Feign,首先要了解 Feign 的使用以及執行過程,而後看 Sentinel 如何整合進去。數組
須要兩個步驟:微信
@EnableFeignClients
註解開啓 Feign 功能@SpringBootApplication @EnableFeignClients // 開啓 Feign 功能 public class MyApplication { ... }
@EnableFeignClients
屬性介紹:app
value:String[] 包路徑。好比 org.my.pkg
,會掃描這個包路徑下帶有 @FeignClient
註解的類並處理;ide
basePackages:String[] 跟 value 屬性做用一致;性能
basePackageClasses:Class<?>[] 跟 basePackages 做用一致,basePackages 是個 String 數組,而 basePackageClasses 是個 Class 數組,用於掃描這些類對應的 package;ui
defaultConfiguration:Class<?>[] 默認的配置類,對於全部的 Feign Client,這些配置類裏的配置都會對它們生效,能夠在配置類裏構造 feign.codec.Decoder
, feign.codec.Encoder
或 feign.Contract
等bean;
clients:Class<?>[] 表示 @FeignClient
; 註解修飾的類集合,若是指定了該屬性,那麼掃描功能相關的屬性就是失效。好比 value、basePackages 和 basePackageClasses;
@FeignClient
註解修飾接口,這樣會基於跟接口生成代理類@FeignClient(name = "service-provider") public interface EchoService { @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET) String echo(@PathVariable("str") String str); }
只要確保這個被 @FeignClient
註解修飾到的接口能被 @EnableFeignClients
註解掃描到,就會基於 java.lang.reflect.Proxy
根據這個接口生成一個代理類。
生成代理類以後,會被注入到 ApplicationContext
中,直接 AutoWired 就能使用,使用的時候調用 echo
方法就至關因而發起一個 Restful 請求。
@FeignClient
屬性介紹:
value:String 服務名。好比 service-provider
, http://service-provider
。好比 EchoService
中若是配置了 value=service-provider
,那麼調用 echo
方法的 url 爲 http://service-provider/echo
;若是配置了 value=https://service-provider
,那麼調用 echo
方法的 url 爲 https://service-provider/divide
serviceId:String 該屬性已過時,但還能用。做用跟 value 一致
name:String 跟 value 屬性做用一致
qualifier:String 給 FeignClient 設置 @Qualifier
註解
url:String 絕對路徑,用於替換服務名。優先級比服務名高。好比 EchoService
中若是配置了 url=aaa
,那麼調用 echo
方法的 url 爲 http://aaa/echo
;若是配置了 url=https://aaa
,那麼調用 echo
方法的 url 爲 https://aaa/divide
decode404:boolean 默認是 false,表示對於一個 http status code 爲 404 的請求是否須要進行 decode,默認不進行 decode,當成一個異常處理。設置爲true以後,遇到 404 的 response 仍是會解析 body
configuration:Class<?>[] 跟 @EnableFeignClients
註解的 defaultConfiguration
屬性做用一致,可是這個對於單個 FeignClient 的配置,而 @EnableFeignClients
裏的 defaultConfiguration
屬性是做用域全局的,針對全部的 FeignClient
fallback:Class<?> 默認值是 void.class
,表示 fallback 類,須要實現 FeignClient 對應的接口,當調用方法發生異常的時候會調用這個 Fallback 類對應的 FeignClient 接口方法。
若是配置了 fallback 屬性,那麼會把這個 Fallback 類包裝在一個默認的 FallbackFactory
實現類 FallbackFactory.Default
上,而不使用 fallbackFactory 屬性對應的 FallbackFactory
實現類
fallbackFactory:Class<?> 默認值是 void.class
,表示生產 fallback 類的 Factory,能夠實現 feign.hystrix.FallbackFactory
接口,FallbackFactory
內部會針對一個 Throwable
異常返回一個 Fallback 類進行 fallback 操做
path:String 請求路徑。 在服務名或 url 與 requestPath 之間
primary:boolean 默認是 true,表示當前這個 FeignClient 生成的 bean 是不是 primary。
因此若是在 ApplicationContext
中存在一個實現 EchoService
接口的 Bean,可是注入的時候並不會使用該Bean,由於 FeignClient 生成的 Bean 是 primary
瞭解了 Feign 的使用以後,接下來咱們來看 Feign 構造一個 Client 的過程。
從 @EnableFeignClients
註解能夠看到,入口在該註解上的 FeignClientsRegistrar
類上,整個鏈路是這樣的:
從這個鏈路上咱們能夠獲得幾個信息:
1.@FeignClient
註解修飾的接口最終會被轉換成 FeignClientFactoryBean
這個 FactoryBean
,FactoryBean
內部的 getObject 方法最終會返回一個 Proxy
2.在構造 Proxy 的過程當中會根據 org.springframework.cloud.openfeign.Targeter
接口的 target
方法去構造。若是啓動了hystrix開關(feign.hystrix.enabled=true
),會使用 HystrixTargeter
,不然使用默認的 DefaultTargeter
3.Targeter
內部構造 Proxy 的過程當中會使用 feign.Feign.Builder
去調用它的 build
方法構造 feign.Feign
實例(默認只有一個子類 ReflectiveFeign
)。
若是啓動了 hystrix 開關(feign.hystrix.enabled=true
),會使用 feign.hystrix.HystrixFeign.Builder
,不然使用默認的feign.Feign.Builder
4.構造出 feign.Feign
實例以後,調用 newInstance
方法返回一個 Proxy
簡單看下這個 newInstance
方法內部的邏輯:
public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if(Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } // 使用 InvocationHandlerFactory 根據接口的方法信息和 target 對象構造 InvocationHandler InvocationHandler handler = factory.create(target, methodToHandler); // 構造代理 T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler); for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; }
這裏的 InvocationHandlerFactory
是經過構造 Feign
的時候傳入的:
DefaultTargeter
: 那麼會使用 feign.InvocationHandlerFactory.Default
這個 factory,而且構造出來的 InvocationHandler
是 feign.ReflectiveFeign.FeignInvocationHandler
HystrixTargeter
: 那麼會在feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory<?>)
方法中調用父類的 invocationHandlerFactory
方法傳入一個匿名的 InvocationHandlerFactory
實現類,該類內部構造出的 InvocationHandler
爲 HystrixInvocationHandler
理解了 Feign 的執行過程以後,Sentinel 想要整合 Feign,能夠參考 Hystrix 的實現:
1.❌ 實現 Targeter
接口 SentinelTargeter
。 很不幸,Targeter
這個接口屬於包級別的接口,在外部包中沒法使用,這個 Targeter
沒法使用。不要緊,咱們能夠沿用默認的 HystrixTargeter
(實際上會用 DefaultTargeter
,下文 Note 有解釋)
2.✅ FeignClientFactoryBean
內部構造 Targeter
、feign.Feign.Builder
的時候,都會從 FeignContext
中獲取。因此咱們沿用默認的 DefaultTargeter
的時候,內部使用的 feign.Feign.Builder
可控,並且這個 Builder 不是包級別的類,可在外部使用
SentinelFeign.Builder
繼承 feign.Feign.Builder
,用來構造 Feign
SentinelFeign.Builder
內部須要獲取 FeignClientFactoryBean
中的屬性進行處理,好比獲取 fallback
, name
, fallbackFactory
。很不幸,FeignClientFactoryBean
這個類也是包級別的類。不要緊,咱們知道它存在在 ApplicationContext
中的 beanName, 拿到 bean 以後根據反射獲取屬性就行(該過程在初始化的時候進行,不會在調用的時候進行,因此不會影響性能)
SentinelFeign.Builder
調用 build
方法構造 Feign
的過程當中,咱們不須要實現一個新的 Feign
,跟 hystrix 同樣沿用 ReflectiveFeign
便可,在沿用的過程當中調用父類 feign.Feign.Builder
的一些方法進行改造便可,好比 invocationHandlerFactory
方法設置 InvocationHandlerFactory
,contract
的調用3.✅ 跟 hystrix 同樣實現自定義的 InvocationHandler
接口 SentinelInvocationHandler
用來處理方法的調用
4.✅ SentinelInvocationHandler
內部使用 Sentinel 進行保護,這個時候涉及到資源名的獲取。SentinelInvocationHandler
內部的 feign.Target
能獲取服務名信息,feign.InvocationHandlerFactory.MethodHandler
的實現類 feign.SynchronousMethodHandler
能拿到對應的請求路徑信息。
很不幸,feign.SynchronousMethodHandler
這個類也是包級別的類。不要緊,咱們能夠自定義一個 feign.Contract
的實現類 SentinelContractHolder
在處理 MethodMetadata
的過程把這些 metadata 保存下來(feign.Contract
這個接口在 Builder 構造 Feign 的過程當中會對方法進行解析並驗證)。
在 SentinelFeign.Builder
中調用 contract
進行設置,SentinelContractHolder
內部保存一個 Contract
使用委託方式不影響原先的 Contract
過程
Note: spring-cloud-starter-openfeign
依賴內部包含了 feign-hystrix
。因此是說默認使用 HystrixTargeter
這個 Targeter
,進入 HystrixTargeter
的 target
方法內部一看,發現有段邏輯這麼寫的:
@Override public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { // 若是 Builder 不是 feign.hystrix.HystrixFeign.Builder,使用這個 Builder 進行處理 // 咱們默認構造了 SentinelFeign.Builder 這個 Builder,默認使用 feign-hystrix 依賴也沒有什麼問題 return feign.target(target); } feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign; ... }
在 SentinelInvocationHandler
內部咱們對資源名的處理策略是: http方法:protocol://服務名/請求路徑跟參數
好比這個 TestService
:
@FeignClient(name = "test-service") public interface TestService { @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET) String echo(@PathVariable("str") String str); @RequestMapping(value = "/divide", method = RequestMethod.GET) String divide(@RequestParam("a") Integer a, @RequestParam("b") Integer b); }
echo
方法對應的資源名:GET:http://test-service/echo/{str}
divide
方法對應的資源名:GET:http://test-service/divide
1.Feign 的內部不少類都是 package 級別的,外部 package 沒法引用某些類,這個時候只能想辦法繞過去,好比使用反射
2.目前這種實現有風險,萬一哪天 starter 內部使用的 Feign 相關類變成了 package 級別,那麼會改造代碼。因此把 Sentinel 的實現放到 Feign 裏並給 Feign 官方提 pr 可能更加合適
3.Feign的處理流程仍是比較清晰的,只要可以理解其設計原理,咱們就能容易地整合進去
歡迎你們對整合方案進行討論,並能給出不合理的地方,固然能提pr解決不合理的地方就更好了。
Sentinel Starter 整合 Feign 的代碼目前已經在 github 倉庫上,可是沒未發版。預計月底發版,若是如今就想使用,能夠在 pom 中引入 Spring SNAPSHOT 的 repository 或自行下載源碼進行編譯。
最後再附上一個使用 Nacos 作服務發現和 Sentinel 作限流的 Feign 例子。
https://github.com/spring-clo...
最後,在Java技術棧微信公衆號後臺回覆:cloud,可獲取棧長整理的一系列 Spring Cloud 教程,目前大量教程還在撰寫中……